二维前缀和的一些整理

前言:
二维前缀和是一维前缀和在二维上的体现,要想了解清楚二维前缀和,最好先学习一维前缀和,在逐步深入二维前缀和。因此我们先从一维的开始讲起,另外一个容易忽视的点是,前缀和优化的是查询复杂度,而不是总体的复杂度,请注意这个区别。
一维前缀和
我们先从问题引入其概念:

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

这个问题我们可以很容易的想到一个O(n)的解决方案,从数组左端开始扫描,在扫描索引到i开始,用一个变量依次与每个元素相加,直到指针扫过j。
但是有没有更快的方法呢,而一维前缀和就是来解决这一类的问题,它的时间复杂度是O(1),我们创建一个数组f,f[i]表示nums[0:i - 1]之和,例如:nums[0]=1,nums[1]=2,那么f[2]=3,f[i]中的i我们可以把它比作nums数组前i个数字相加。而f[0]显然等于0.因此对于有n个数的数组nums,我们可以设置一个大小为n+1的数组f。f[j + 1]-f[i],就表示索引i到j的元素总和。

一维前缀和用处有很多,比如它是计数排序和基数排序(前者的进阶)的核心。

代码实现:

class NumArray {
    //sums[i]表示前nums[0],...,nums[i-1]之和
    int[] sums;

    public NumArray(int[] nums) {
        int n = nums.length;
        sums = new int[n+1];
        for(int i=0; i<n; i++){
            sums[i+1] = sums[i] + nums[i];
        }
    }
    
    public int sumRange(int i, int j) {
        return sums[j+1]-sums[i];
    }
}

二维前缀和的引入

现在我们知道一维前缀和的含义及基本使用场合,我们现在来回答下一个问题。
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。
解析:暴力的做法是我们依次遍历row1到row2行,并用一个变量来存储结果。这样我们每次查询的时间复杂度达到了O(mn)。但我们仔细想一想,其实这个问题我们也可以用一维前缀和来解决,每一个矩阵其实都是一排排的一维数组组成的,因此我们可以为每一排创建一维的前缀和数组f,每行的和用f计算,这样我们每次查询的时间复杂度就降到了O(m),但是要考虑的时我们初始化前缀和数组需要O(mn),因为一共有m排需要分别初始化。
代码实现:

class NumMatrix {
    int[][] sums;

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

从前面可知一维的前缀和做法并没有使查询的效率达到O(1),那有没有做法达到呢?这时我们就有了今天的主题,二维前缀和。我们可以想象成一维干一维的事,二维干二维的事。
我们通过类比,定义f(i,j) 为矩阵matrix的以(i,j)为右下角的子矩阵的元素之和。但我们在实际设置这个数组时,f[i,j]的含义设为有i行,j列的矩阵的元素之和,而每次查询的行列参数都是下标值(从0开始),所以在使用f数组时需要加1,这一点很重要。
那在f数组的含义下,我们怎么计算f数组呢?
当i=0或j=0时,f[i,j]显然等于0
当i或j都大于0时,我们假设计算f[i,j]时已经知道了f[i-1,j],f[i,j-1]和f[i-1,j-1]的值,那么我们可以通过一张图来知道我们怎么利用他们
在这里插入图片描述
从图可以自己推导出:f(i,j)=f(i−1,j)+f(i,j−1)−f(i−1,j−1)+matrix[i][j]

计算查询结果
而对于每次查询sumRegion(row1,col1,row2,col2),当row1=0且col1=0时,则退化成了计算f[row1+1][col1+1]
的值。

在一般情况,当row1≤row2且col1≤col2时,我们可以通过下面这张图看出计算过程:
在这里插入图片描述
有了上述的推导,我们即可在O(1)时间内得到sumRegion(row 1, col1 , row2 , col2)的值。
代码实现部分:

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];
    }
};

下面我们来看在使用二维前缀和的基础上,如何去解决一个问题。
给你一个 m x n 的矩阵 matrix 和一个整数 k ,找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。
从题目中我们可以通过枚举矩阵左上角和右下角(即枚举每个可能的子矩形),通过二维前缀和数组算出每个矩阵和是否满足条件。这是一种朴素的二维前缀和方法。
代码实现:

class Solution {
    public int maxSumSubmatrix(int[][] matrix, int k) {
        int m = matrix.length, n = matrix[0].length;
        int[][] sum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
        int ans = Integer.MIN_VALUE;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {//左上角(i,j)
                for (int p = i; p <= m; p++) {
                    for (int q = j; q <= n; q++) {//右下角(p,q)
                        int cur = sum[p][q] - sum[i - 1][q] - sum[p][j - 1] + sum[i - 1][j - 1];
                        if (cur <= k)
                            ans = Math.max(ans, cur);
                    }
                }
            }
        }
        return ans;
    }
}

但这种方法容易超时,且我们有优化的方案。
但是由于优化方法需要二分+有序集合的知识,我们下次说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值