初学算法性价比最高的前缀和算法中章

489 阅读6分钟

算法学习专栏,本篇前缀和系列算法学习的中章,初章对应的技法练习已经更新「前缀和算法技法初章」。算法学习需要投入大量的时间和精力,不断的练习。本人建议从前缀和开始学习,理由是前缀和算法不需要其他算法或者数据结构为基础,适用于算法初学者(熟悉一门语言的编写。)通过学习,可以体会到算法之美(难度较低,收益较高同时非常实用的算法,直接甩掉其他竞争者 狗头。)~~

前言

通过初章我们已经了解到了什么是「前缀和」,它的「适用场景」是什么?时间空间复杂度是多少?首先我们来回顾一下。前缀和由两部分组成

  1. 前缀

通过构建一个「前缀和数组」preSum从而优化计算数组「区间和」的效率。(O(N)>O(1)O(N)->O(1))。计算区间[left,right]的区间和,只需要:

  • preSum[right+1] - preSum[left]即可得到。

计算区间和的时间复杂度大大提升为O(1)O(1),但是我们需要初始化构建一个前缀和数组所需的时间复杂度是O(N)O(N),同时需要额外的空间复杂度O(N)O(N)属于「空间换时间」。不过正如一直说明的,现如今「空间」的消耗是微不足道的,空间的提升往往比提升算力更节省成本,所以问题是不大的。

虽然前缀和适用于计算「区间和」的场景,但是它还是存在条件的。只能够计算数组元素「不发生改变」的数组,否则每次数组元素发生改变,我们都需要重新构建「前缀和数组」。当然这是存在对应的数据结构能解决此类问题的,这都是后话了。同时我们目前所学的「前缀和算法」所涉及到的数组,都有一个共同点,那就是都是「一维数组」,那么前缀和算法也是能适用于「二维数组」的场景的。各位少侠,我们来看下面这道例题~

二维数组的前缀和

304. 二维区域和检索 - 矩阵不可变 - 力扣(LeetCode)

image.png

题目给定了一个二维数组matrix同时给出两个坐标,分别是子矩阵的:

  1. 左上角(row1,col1)
  2. 右下角(row2,col2)

让我们计算出这个子矩阵范围内的元素总和。下图就是左上角(1,2),右下角(3,4)红色子矩阵。

image.png

明白了这道题的题意,那么我们来想一下怎么解答呢?

首先最容易想到的就是「暴力」~ 不要害怕自己只能想到暴力的解法,其实很多时候题目能想到暴力解法,其实是很不错了,至少你对这道题目的题意已经理解了。那么剩下的就是通过我们所学的各种「算法」和「数据结构」来优化这个暴力的过程。

image.png

我们来暴力的使用两个for循环,外层循环控制「行」,内层循环控制「列」

  • 行的遍历:row1->row2
  • 列的遍历:col1->col2

然后通过遍历给出的子矩阵累加得到它们的元素总和。

class NumMatrix {
    int[][] arr;
    public NumMatrix(int[][] matrix) {
        //赋值matrix给arr
        arr = matrix;
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        int sum = 0;
        //双层for循环遍历
        for(int i = row1;i<=row2;++i){
            for(int j=col1;j<=col2;++j){
                sum += arr[i][j];
            }
        }
        return sum;
    }
}

这道题目的数据量分别是

  • 行m为200
  • 列n为200

我们的时间复杂度是O(N2)O(N^2),所以是41044*10^4,而sumRegion()方法的调用次数没有给出,所以不能确定是否能通过。我们尝试提交以下~

image.png

耗时2102ms,有惊无险~

image.png

虽然通过了,但是还是比较勉强的。所以我们还有什么方法能优化这个计算效率吗?答案就是:前缀和算法。我们可以发现这道题的本质还是求「前缀和」不过这是数组变成了二维。

定义

那么同一维前缀和一样,我们首先来定义「二维前缀和数组」

  • 数组的长度为[m+1][n+1]
  • 前缀和preSum[i][j]:定义为在「原数组」的左上角为(0,0)右下角为(i-1,j-1)的子矩阵范围内的元素之和。

以下就是preSum(4,5)在「原数组」所代表的子矩阵范围元素之和。

image.png

同一维前缀和数组一样,我们的数组长度是相比原来的数组长度加1即[m+1][n+1]。preSum[i][j]定义为在「原数组」的左上角为(0,0)右下角为(i-1,j-1)的子矩阵范围内的元素之和。 这点非常重要,大家要记得,不要搞混「前缀和数组」下标 对应「原数组下标」之间的关系。

构建

「一维前缀和数组」通过「原数组」构建是非常容易想到的且符合直觉的,那么「二维前缀和数组」应该怎么构建呢?

同「一维前缀数组」构建一样,我们是需要依靠前面构建好的前缀和数组元素,不过不同的是依靠的比较多一点,从一个变成了三个。如下图,如果我们想计算前面表示的preSum(4,5) image.png

我们应该怎么依靠前面计算好的preSum(i,j)

  • i的范围为:[0,4]
  • j的范围为:[0,5]

其实也很简单,我们首先通过preSum[i-1][j]+preSum[i][j-1],通过图可以看到它们在「原数组」代表的子矩阵。

image.png

两者相加我们可以得到以下包含以下元素的图形:

image.png

对比preSum(4,5)我们还漏了一个元素,即「原数组」所代表的matrix[3][4]所以我们需要加上。

同时前面两个相加,导致了一块区域preSum[3,4]被重复添加了,我们需要剔除掉。

image.png

pre[4,5] = pre[3][5]+pre[4][4]-pre[3][4]+matrix[3][4]

即可以得出:

  • preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] - preSum[i-1][j-1] + matrix[i-1][j-1]
  • i和j从下标1开始。

计算

构建好了「前缀和数组」那么我们怎么计算对应范围内的元素和呢?

根据我们构建的前缀和数组的定义

  • preSum[i][j]定义为在「原数组」的左上角为(0,0)右下角为(i-1,j-1)的子矩阵范围内的元素之和。

由于我们的左上角是固定为(0,0)的,同时我们可以使用和「构建」一样的思想,通过对应相关计算,得出符合条件范围的元素。

例如求出

  • 左上角(2,1)
  • 右下角(4,3) 子矩阵的元素之和。

image.png

我们可以使用一个大的子矩阵,包含了所需要的元素,即preSum(5,4)(记住定义,preSum的下标对应原数组的下标需要减1)

image.png

然后减去对应不需要的元素,根据定义「前缀和数组」的左上角是固定为(0,0)。所以我们需要减去的两个preSum坐标 pre(5,1)pre(2,4)

image.png

更为直观的表示的话就是这样

image.png

红色区域为我们重复减掉的元素区域,我们需要加上pre[2,1],此时剩下的紫色区域就是我们所需的元素之和。

sumRegion(2,1,4,3) = pre[5,4] - pre[5,1] - pre[2,4] + pre[2,1]

  • sumRegion(r1,c1,r2,c2) = pre[r2+1][c2+1] - pre[r1][c2+1] - pre[r2+1][c1] + pre[r1][c1]

image.png

代码

class NumMatrix {
    int[][] preSum;
    public NumMatrix(int[][] matrix) {
        int m = matrix.length,n = matrix[0].length;
        preSum = new int[m+1][n+1];
        for(int i =1;i<=m;++i){
            for(int j =1;j<=n;++j){
                preSum[i][j] = preSum[i-1][j]+preSum[i][j-1]-preSum[i-1][j-1]+matrix[i-1][j-1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return preSum[row2+1][col2+1]-preSum[row1][col2+1]-preSum[row2+1][col1] + preSum[row1][col1];
    }
}

image.png

时间大大缩减。

  • 时间复杂度:构建为O(MN)O(M*N), sumRegion为O(1)O(1)
  • 空间复杂度为O(MN)O(M*N)

总结

  1. 二维前缀和数组长度为[m+1][n+1]。
  2. preSum[i][j]定义为在「原数组」的左上角为(0,0)右下角为(i-1,j-1)的子矩阵范围内的元素之和。
  3. 任何子矩阵都可以通过之前的推导而来。
  4. 构建前缀和公式为:preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] - preSum[i-1][j-1] + matrix[i-1][j-1](i和j从下标1开始)。
  5. 通过前缀和计算子矩阵元素之和公式为:sumRegion(r1,c1,r2,c2) = pre[r2+1][c2+1] - pre[r1][c2+1] - pre[r2+1][c1] + pre[r1][c1]

image.png