前缀和算法系列

理论

前缀和其实就是 动态规划 的思想,因为具体题目动态规划的思路画成表格来看,就是类似前缀和的样子:不断根据前面算出的值 做同样某个或某几个操作,累积计算。最后 利用滚动算出来的值 快速得出想要的答案。

preSum(前缀和) 方法能快速计算指定区间段 i - j 的元素之和。它的计算方法是从左向右遍历数组,当遍历到数组的 i
位置时,preSum 表示 i 位置左边的元素之和。

下面根据实际题目理解一下~

例题

leetcode303. 区域和检索 - 数组不可变

给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

实现 NumArray 类:

NumArray(int[] nums) 使用数组 nums 初始化对象 int sumRange(int i, int j) 返回数组
nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], … , nums[j]))

思路分析

这是一道前缀和的裸题。

当需要我们求「某一段」区域的和的时候,我们要很自然的想到「前缀和」。

前缀和的作用就是为了帮助我们快速求某一段的和,是「差分」的逆运算。

前缀和数组 sum 的每一位记录的是当前位置距离起点位置这连续一段的和区间和

因此当我们要求特定的一段 [i,j] 的区域和的时候,可以直接利用前缀和数组快速求解:ans = sum[j] - sum[i - 1]

在这里插入图片描述

  • 由于涉及 -1 操作,为了减少一些边界处理,我们可以使前缀和数组下标从 1 开始记录,然后在进行答案计算的时候,根据源数组下标是否从 1 开始决定是否产生相应的偏移:
class NumArray {
    int[] sum;
    public NumArray(int[] nums) {
        int n = nums.length;
        // 前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
        sum = new int[n + 1];
        // 预处理除前缀和数组(模板部分)
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
    }
    public int sumRange(int i, int j) {
        // 求某一段区域和 [i, j] 的模板是 `sum[j] - sum[i - 1]`(模板部分)
        // 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
        // `sum[j + 1] - sum[i - 1 + 1] = sum[j + 1] - sum[i]`
        return sum[j + 1] - sum[i];
    }
}
  • 也可以仅按照n的大小开,只是下标为0的情况要单独拎出来写~
class NumArray {
public:
    vector<int> sum;
public:
    NumArray(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return;
        sum.resize(n);
        sum[0] = nums[0];
        //求前缀和可快速找到i~j的和,不用每次都便利求和一遍
        for(int i = 1; i < n; i++){
            sum[i] = sum[i-1] + nums[i];
        }
    }
    
    int sumRange(int i, int j) {
        if(i == 0) return sum[j];
        return sum[j] - sum[i-1];
    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * int param_1 = obj->sumRange(i,j);
 */

事实上,前缀和思想不仅能应用在一维,还能应用在多维上。

见下题:

leetcode304. 二维区域和检索 - 矩阵不可变

给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。
在这里插入图片描述
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为8。

思路分析

  • 做这种初始化一次、检索多次的题目的秘诀:在初始化的时候做预处理。
1.暴力法

对二维矩阵,求子矩阵 (n*m) 的和。暴力法就是两重循环,累加求和。

每次查询花费 O(n*m)O(n∗m) 时间,n和m是子矩阵的行数和列数。查询的代价有点大。

1.第一步优化:一维前缀和

上面的暴力法其实也分了 n 步:第一行的求和,到第 n 行的求和

它们分别是 n 个一维数组。

上一题学习了一维前缀和,我们可以对这n个一维数组求前缀和,得到n个一维preSum数组。

为了节省查询的时间,我们求出整个矩阵每一行的一维preSum数组

根据定义 preSum[i] = nums[0] + nums[1] +…+nums[i],求出下图红字部分:
在这里插入图片描述
然后套用通式:nums[i]+…+nums[j]=preSum[j]-preSum[i-1] 就可以求出粉色子阵列的和,如下图,两个黄圈的前缀和,减去所在行的绿圈的前缀和,相加。
可见,如果想每次查询一个子阵列的和,我们可以提前求出每一行数组的一维前缀和。
查询一行的子数组的和,就是 O(1) 的复杂度
那么查询 n 行的子阵列时,就是每次查询花费 O(n),比O(n^2) 好

class NumMatrix {
public:
    vector<vector<int>> preSum;//前缀和二维数组
public:
    NumMatrix(vector<vector<int>>& matrix) {
        int m = matrix.size();//m行n列
        if(m == 0) return;//记得先判断行,行为0,列就无意义,获取会报错
        int n = matrix[0].size();
        preSum.resize(m, vector<int>(n+1));

        for(int row = 0; row < m; row++){
            for(int col = 1; col < n+1; col++){
                preSum[row][col] = preSum[row][col-1] + matrix[row][col-1];//按行计算一维前缀和
            }
        }
    }
    
    int sumRegion(int row1, int col1, int row2, int col2) {
        int sum = 0;
        for(int i = row1; i <=row2; i++){
            sum += preSum[i][col2+1] - preSum[i][col1];
        }
        return sum;
    }
};
2.第二步优化:二维前缀和
  • 步骤一:求 preSum

我们先从如何求出二维空间的 preSum[i][j]。 我们定义 preSum[i][j] 表示 从[0,0] 位置到 [i,j] 位置的子矩形所有元素之和。
可以用下图帮助理解:

  S(O, D) = S(O, C) + S(O, B) - S(O, A) + D
  S(O,D)=S(O,C)+S(O,B)−S(O,A)+D

在这里插入图片描述

减去 S(O, A) 的原因是 S(O, C) 和 S(O, B) 中都有 S(O, A),即加了两次 S(O, A),所以需要减去一次 S(O, A)。
如果用 preSum 表示的话,对应了以下的递推公式:

preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + matrix[i][j]
  • 步骤二:根据 preSum 求子矩形面积

前面已经求出了数组中从 [0,0] 位置到 [i,j] 位置的 preSum。下面要利用preSum[i][j] 来快速求出任意子矩形的面积。
同样利用一张图来说明:

  S(A, D) = S(O, D) - S(O, E) - S(O, F) + S(O, G) 

在这里插入图片描述

加上子矩形 S(O, G) 面积的原因是 S(O, E) 和 S(O, F) 中都有 S(O, G),即减了两次 S(O,G),所以需要加上一次 S(O, G)。
如果要求 [row1, col1] 到 [row2, col2] 的子矩形的面积的话,用preSum 对应了以下的递推公式:

  preSum[row2][col2] - preSum[row2][col1 - 1] - preSum[row1 - 1][col2] + preSum[row1 - 1][col1 - 1]

下面代码实现的时候,使用的 preSum 比原矩阵 matrix 多了一行一列,是为了让第 0 行与第 0 列的元素也能使用上面的递推公式如果 preSum 矩阵大小和 martix 大小相等,则需要对第 0 行与第 0 列特殊判断

class NumMatrix {
public:
    vector<vector<int>> sums;

    NumMatrix(vector<vector<int>>& matrix) {
        int m = matrix.size();
        if (m > 0) {
            int n = matrix[0].size();
            sums.resize(m + 1, vector<int>(n + 1));
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
                }
            }
        }
    }

    int sumRegion(int row1, int col1, int row2, int col2) {
        return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
    }
};

文中图片参考自:https://leetcode-cn.com/problems/range-sum-query-2d-immutable/solution/ru-he-qiu-er-wei-de-qian-zhui-he-yi-ji-y-6c21/ 仅作记录学习~

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值