算法27,二维前缀和

有点难

这份笔记详细记录了二维前缀和 的原理推导,这是算法竞赛和面试中非常经典的技巧。它主要用于解决**"快速计算矩阵(二维数组)中某子矩阵的元素和"**的问题。

1. 核心思想:降维打击

一维数组求区间和,我们用一维前缀和

二维数组(矩阵)求子矩阵和,自然升级为二维前缀和

  • 暴力法:每次查询都遍历子矩阵,复杂度 O(m×n)。

  • 前缀和法 :通过空间换时间,预处理出 dp数组,使得单次查询复杂度降为 O(1)。

2. 推导过程解析(看图说话)

(1) 定义:dp[i][j]是什么?

笔记中明确定义:dp[i][j]表示从 (1,1)到 (i,j)这个矩形区域内所有元素的和

也就是以左上角为原点,右下角为 (i,j)的子矩阵的总和。

(2) 状态转移方程(最难理解的一步)

笔记中画了一个 2×2的小矩阵,标记为 A、B、C、D,这是推导的关键:

  • 目标 :求右下角的块 D

  • 已知

    • dp[i−1][j]:包含了 A、B 的大矩形和。

    • dp[i][j−1]:包含了 A、C 的大矩形和。

    • arr[i][j]:就是 D 的值。

    • dp[i−1][j−1]:包含了 A 的矩形和(多加了一次,需要减掉)。

推导逻辑

如果我们把上面两个大矩形加起来(dp[i−1][j]+dp[i][j−1]),中间的 A 区域被加了两次

所以,我们需要减去一次 A(即 dp[i−1][j−1]),再加上当前格子的右下角 D(即 arr[i][j])。

得出公式

复制代码
dp[i][j]=dp[i−1][j]+dp[i][j−1]+arr[i][j]−dp[i−1][j−1]

(注:这个公式的前提是输入数组 arr的下标也从 1 开始,且 dp[0][x]dp[x][0]都为 0)

3. 查询逻辑(容斥原理)

笔记下半部分画了一个大方框套小方框的图,用来解释如何查询任意子矩阵 (x1​,y1​)到 (x2​,y2​)的和。

类比一维前缀和(sum[l...r]=dp[r]−dp[l−1]),二维查询就是"大盒子减小盒子加重叠":

复制代码
Sum=dp[x2​][y2​]−dp[x1​−1][y2​]−dp[x2​][y1​−1]+dp[x1​−1][y1​−1]
  • dp[x2​][y2​]:包含目标区域的超大矩形总面积。

  • 减去左边多余列 (dp[x1​−1][y2​]) ​ 和 上边多余行 (dp[x2​][y1​−1])

  • 加上左上角多减的一次 (dp[x1​−1][y1​−1])

4. 代码实现(Java 版)

结合笔记中的逻辑,以下是标准的 Java 实现代码:

复制代码
public class TwoDPrefixSum {
    public static void main(String[] args) {
        // 假设输入数据如下(下标从1开始,补0方便计算)
        // 3 4 表示 3行 4列
        int[][] arr = {
            {0, 0, 0, 0, 0}, // 第0行填充0,作为哨兵节点
            {0, 1, 2, 3, 4}, // 第1行数据
            {0, 5, 6, 7, 8}, // 第2行数据
            {0, 9, 10, 11, 12} // 第3行数据
        };
        
        int m = 3, n = 4;
        long[][] dp = new long[m + 1][n + 1];

        // 1. 预处理(构建 dp 数组)
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 套用笔记中的公式
                dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1];
            }
        }

        // 2. 查询示例:求 (2,2) 到 (3,3) 的子矩阵和
        int x1 = 2, y1 = 2;
        int x2 = 3, y2 = 3;
        
        long result = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1];
        
        System.out.println("子矩阵和为: " + result); // 预期结果:6+7+10+11 = 34
    }
}

5. 总结与避坑指南

  1. 下标统一 :笔记左侧问"为什么下标从1开始?",这是因为这样可以完美避开数组越界,并且让 dp[0][...]dp[...][0]自然变为 0,简化计算。

  2. 数据类型 :笔记中特意将 dp数组声明为 long类型。这是因为矩阵求和很容易发生整数溢出 ,如果输入数据是 int且矩阵很大,求和结果可能会超过 int的最大值,必须使用 long

  3. 复杂度

    • 预处理:O(m×n)

    • 单次查询:O(1)

相关推荐
_深海凉_6 小时前
LeetCode热题100-验证二叉搜索树
算法·leetcode·职场和发展
Mr.朱鹏6 小时前
9-检索增强生成RAG详解
python·gpt·langchain·大模型·llm·rag
蒟蒻的贤6 小时前
编译原理里的冲突到底是什么?
考研·算法
IpdataCloud6 小时前
企业安全运营中,如何用IP风险识别工具快速发现异常终端?操作指南
开发语言·php
码界筑梦坊6 小时前
125-基于Flask的客户购物偏好数据可视化分析系统
python·信息可视化·flask·毕业设计
兩尛6 小时前
C++多线程,并发
java·开发语言
_深海凉_6 小时前
LeetCode热题100-二叉树的右视图
算法·leetcode·职场和发展
测试秃头怪6 小时前
接口测试与常用接口测试工具详解
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
坐吃山猪6 小时前
【Nanobot】README03_LEVEL2_工具系统架构
python·源码·agent·nanobot