每周一算法:二维前缀和

二维前缀和

对一个序列预处理得到前缀和数组,可以在 O ( 1 ) O(1) O(1)的时间复杂度计算序列中任意区间的元素之和,这是前缀和算法的作用。而二维前缀和是用来优化处理子矩阵的和。

例如,对于矩阵 A = [ 1 4 5 2 9 5 2 1 6 9 8 3 4 2 1 6 ] A=\left[ \begin{matrix}1 & 4 & 5 & 2\\ 9 & 5 & 2 & 1 \\ 6 & 9 & 8 & 3 \\ 4 & 2 &1 &6\end{matrix} \right] A= 1964459252812136 中的一个子矩阵 [ 5 2 9 8 ] \left[ \begin{matrix} 5 & 2 \\ 9 & 8 \end{matrix} \right] [5928]的所有元素之和为 24 24 24

使用二维前缀和可以在 O ( 1 ) O(1) O(1)的时间复杂度计算出该子矩阵的和。

算法思想

构造前缀和数组

首先需要在原矩阵的基础上预处理一个二维前缀和数组 s [ ] [ ] s[][] s[][],其中 s [ i ] [ j ] s[i][j] s[i][j]表示从第 1 1 1行第 1 1 1列开始到第 i i i行第 j j j列所有数的和。

例如,对于矩阵 A = [ 1 4 5 2 9 5 2 1 6 9 8 3 4 2 1 6 ] A=\left[ \begin{matrix}1 & 4 & 5 & 2\\ 9 & 5 & 2 & 1 \\ 6 & 9 & 8 & 3 \\ 4 & 2 &1 &6\end{matrix} \right] A= 1964459252812136

  • s [ 1 ] [ 1 ] s[1][1] s[1][1]表示矩阵第 1 1 1行第 1 1 1列所有数的和,为 1 1 1
  • s [ 1 ] [ 2 ] s[1][2] s[1][2]表示矩阵第 1 1 1行第 1 1 1列到第 1 1 1行第 2 2 2列所有数的和,为 1 + 4 = 5 1+4=5 1+4=5
  • s [ 3 ] [ 3 ] s[3][3] s[3][3]该如何计算呢?

在这里插入图片描述

分析下图可以发现, s [ 3 ] [ 3 ] s[3][3] s[3][3]由三部分组成

  • 绿色部分 s [ 2 ] [ 3 ] s[2][3] s[2][3]
  • 红色部分 s [ 3 ] [ 2 ] s[3][2] s[3][2]
  • 紫色部分 A [ 3 ] [ 3 ] A[3][3] A[3][3]

在这里插入图片描述
由于 s [ 2 ] [ 2 ] s[2][2] s[2][2](黄色部分)被包含了两次,因此在计算 s [ 3 ] [ 3 ] s[3][3] s[3][3]时,需要去掉一个 s [ 2 ] [ 2 ] s[2][2] s[2][2]。因此, s [ 3 ] [ 3 ] = s [ 2 ] [ 3 ] + s [ 3 ] [ 2 ] − s [ 2 ] [ 2 ] + A [ 3 ] [ 3 ] s[3][3]=s[2][3]+s[3][2]-s[2][2]+A[3][3] s[3][3]=s[2][3]+s[3][2]s[2][2]+A[3][3]

那么,从第 1 1 1行第 1 1 1列开始到第 i i i行第 j j j列所有数的和 s [ i ] [ j ] = s [ i − 1 ] [ j ] + s [ i ] [ j − 1 ] − s [ i − 1 ] [ j − 1 ] + A [ i ] [ j ] s[i][j]=s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + A[i][j] s[i][j]=s[i1][j]+s[i][j1]s[i1][j1]+A[i][j]

在这里插入图片描述

计算子矩阵的和

有了前缀和数组,如何计算矩阵 A = [ 1 4 5 2 9 5 2 1 6 9 8 3 4 2 1 6 ] A=\left[ \begin{matrix}1 & 4 & 5 & 2\\ 9 & 5 & 2 & 1 \\ 6 & 9 & 8 & 3 \\ 4 & 2 &1 &6\end{matrix} \right] A= 1964459252812136 中的一个子矩阵 [ 5 2 9 8 ] \left[ \begin{matrix} 5 & 2 \\ 9 & 8 \end{matrix} \right] [5928]的所有元素之和?

通过下图可以发现,从 ( 2 , 2 ) (2,2) (2,2) ( 3 , 3 ) (3,3) (3,3)的子矩阵之和可以由下面几个部分的和相减得到:

  • 天蓝色部分 s [ 3 ] [ 3 ] s[3][3] s[3][3]
  • 绿色部分 s [ 1 ] [ 3 ] s[1][3] s[1][3]
  • 红色部分 s [ 3 ] [ 1 ] s[3][1] s[3][1]

在这里插入图片描述

由于黄色部分 s [ 1 ] [ 1 ] s[1][1] s[1][1]被减了两次,因此计算结果时需要补加一次 s [ 1 ] [ 1 ] s[1][1] s[1][1]。那么从 ( 2 , 2 ) (2,2) (2,2)开始到 ( 3 , 3 ) (3,3) (3,3)结束的子矩阵之和 = s [ 3 ] [ 3 ] − s [ 1 ] [ 3 ] − s [ 3 ] [ 1 ] + s [ 1 ] [ 1 ] =s[3][3]-s[1][3]-s[3][1]+s[1][1] =s[3][3]s[1][3]s[3][1]+s[1][1]

更一般地,从 ( x 1 , y 1 ) (x1,y1) (x1,y1)开始到 ( x 2 , y 2 ) (x2,y2) (x2,y2)结束的子矩阵之和 = s [ x 2 ] [ y 2 ] − s [ x 1 − 1 ] [ y 2 ] − s [ x 2 ] [ y 1 − 1 ] + s [ x 1 − 1 ] [ y 1 − 1 ] =s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1] =s[x2][y2]s[x11][y2]s[x2][y11]+s[x11][y11]
在这里插入图片描述

算法实现

//构造前缀和数组
for(int i = 1; i <= n; i ++)
	for(int j = 1; j <= m; j ++)
	 	s[i][j] = s[i -1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
//输出(x1,y1)到(x2,y2)子矩阵的和
cout << s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] << endl;	 

时间复杂度

二维前缀和算法的时间复杂度分为两部分:

  • 构造二维前缀和数组: O ( n × m ) O(n\times m) O(n×m)
  • 求子矩阵的和: O ( 1 ) O(1) O(1)

空间复杂度

需要构造和原矩阵大小一样的前缀和数组,因此空间复杂度为 O ( n × m ) O(n\times m) O(n×m)


真题演练

洛谷P2004 领地选择

题目描述

作为在虚拟世界里统帅千军万马的领袖,小 Z 认为天时、地利、人和三者是缺一不可的,所以,谨慎地选择首都的位置对于小 Z 来说是非常重要的。

首都被认为是一个占地 C × C C\times C C×C 的正方形。小 Z 希望你寻找到一个合适的位置,使得首都所占领的位置的土地价值和最高。

输入格式

第一行三个整数 N , M , C N,M,C N,M,C,表示地图的宽和长以及首都的边长。

接下来 N N N 行每行 M M M 个整数,表示了地图上每个地块的价值。价值可能为负数。

输出格式

一行两个整数 X , Y X,Y X,Y,表示首都左上角的坐标。

样例输入

3 4 2
1 2 3 1
-1 9 0 2
2 0 1 1

样例输出

1 2

提示

对于 60 % 60\% 60% 的数据, N , M ≤ 50 N,M\le 50 N,M50

对于 90 % 90\% 90% 的数据, N , M ≤ 300 N,M\le 300 N,M300

对于 100 % 100\% 100% 的数据, 1 ≤ N , M ≤ 1 0 3 1\le N,M\le 10^3 1N,M103 1 ≤ C ≤ min ⁡ ( N , M ) 1\le C\le \min(N,M) 1Cmin(N,M)

解题思路

根据题目描述,在 N × M N\times M N×M的矩阵中求大小为 C × C C\times C C×C子矩阵的土地价值和的最大值,因此可以使用二维前缀和优化。

实现代码

#include <iostream>
using namespace std;
const int N = 1010;
long long s[N][N];
int n, m, c, x;
int main()
{
    cin >> n >> m >> c;
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
        {
            scanf("%d", &x);
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x; 
        }
    long long ans = -1e10;
    int a, b;
    //枚举子矩阵的开始位置,注意子矩阵不能越界
    for(int x1 = 1; x1 + c - 1 <= n; x1 ++)
        for(int y1 = 1; y1 + c - 1 <= n; y1 ++)
        {
            int x2 = x1 + c - 1, y2 = y1 + c - 1;
            int t = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1];
            if(t > ans)
            {
                ans = t, a = x1, b = y1;
            }
        }
    cout << a << ' ' << b << endl;
}

总结

  • 二位前缀和可以优化求子矩阵的和
  • 构造二维前缀和数组:s[i][j] = s[i -1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
  • 使用前缀和数组求子矩阵的和:s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值