利用前缀和,我们可以快速算出某段区间的和。不过前缀和只能处理静态数组,只用于查找,不修改
一、模板
1.一维前缀和
假设有一个数组[L,R],如果一个个算某区间和
S1 = a1
S2 = a1 + a2
...(会有很多不必要的计算),前缀和有点像递推,像如下公式这样计算
Si = Si-1 + ai;
然后如果要找到某一段的区间和
aL + aL+1 + ... +aR =SR - SL-1
下面这段代码大概意思是输入一个长度为n的数组,有m个样例,每个样例给出L,R,输出这个样例的区间和
一维前缀和 #include <iostream> #include <algorithm> #include <string> #include <cstdio> using namespace std; const int N = 100010; int n, m; int a[N];// 表示原数组 int s[N];// 表示前缀和数组 int main() { scanf("%d%d", &n, &m); for(int i = 1;i <= n;i++) { scanf("%d", &a[i]); s[i] = s[i - 1] + a[i]; } while(m --) { int l, r; scanf("%d%d",&l,&r); printf("%d\n",s[r] - s[l - r]); } return 0; }
2.二维前缀和
y1 y2 1 7 2 4 x1 3 6 2 8 x2 2 1 2 3 计算前缀和矩阵
利用容斥原理
Sx,y = Sx-1,y + Sx,y-1 + Sx-1,y-1 + ax,y (Sx-1,y-1减了两次,加回来一次)
计算某子矩阵和(蓝色数字)
S[x1,y1~x2,y2] = Sx2,y2 - Sx2,y1-1 - Sx1-1,y2 + Sx1-1,y1-1
#include <iostream> #include <algorithm> #include <string> #include <cstdio> using namespace std; const int N = 1010; int n, m, q; int a[N][N],s[N][N]; int main() { scanf("%d%d%d",&n,&m,&q); for(int i = 1; i <= n; i ++) for(int j = 1; j <=m; j ++) { scanf("%d",a[i][j]); s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]; } while(q --) { int x1, y1, x2, y2; scanf("%d%d%d%d",&x1,&y1,&x2,&y2); printf("%d\n",s[x2][y2] - s[x1 - 1][y2] -s[x2][y1 - 1] + s[x1 - 1][y1 - 1]); } return 0; }
二、例题
1.激光炸弹
地图上有 N 个目标点,用整数 Xi,Yi 表示目标在地图上的位置,每个目标都有一个价值 Wi。
注意:不同目标可能在同一位置。
现在有一种新型的激光炸弹,可以摧毁一个包含 R×R 个位置的正方形内的所有目标。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y轴平行。
求一颗炸弹最多能炸掉地图上总价值为多少的目标。
输入格式
第一行输入正整数 N 和 R,分别代表地图上的目标数目和正方形包含的横纵位置数量,数据用空格隔开。
接下来 N 行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,Wi,分别代表目标的 x 坐标,y 坐标和价值,数据用空格隔开。
输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。
数据范围
0≤R≤109
0<N≤10000
0≤Xi,Yi≤50000
0≤Wi≤1000输入样例:
2 1 0 0 1 1 1 1
输出样例:
1
#include <iostream> #include <algorithm> #include <string> #include <cstdio> using namespace std; const int N = 5010; int n, m; int s[N][N];//防止mle只开一个数组 int main() { int cnt, r; cin >> cnt >> r; r = min(5001, r); n = m = r; while(cnt --) { int x, y, w; cin >> x >> y >> w; x ++, y ++; n = max(n, x), m = max(m, y); s[x][y] += w; } 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]; int res = 0; //枚举所有边长是r的矩形,枚举(i,j)为右下角 for(int i = r; i <= n; i ++) for(int j = r; j <= m; j ++) res = max(res,s[i][j] - s[i-r][j] - s[i][j-r] + s[i-r][j-r]); cout << res <<endl; return 0; }
注意:目标是坐标轴上的点,不是方块。R * R框出的矩形,边长上的点不算,所以最大能框住点的矩形不是和坐标轴重合,而是错开的,比如这个矩形的顶点在坐标轴中间,不在坐标轴上,这样可以得到最多的目标。
2.k倍区间
给定一个长度为 NN 的数列,A1,A2,…AN,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是 K 倍区间。
你能求出数列中总共有多少个 K 倍区间吗?
输入格式
第一行包含两个整数 N 和 K。
以下 N 行每行包含一个整数 Ai。
输出格式
输出一个整数,代表 K 倍区间的数目。
数据范围
1≤N,K≤100000,
1≤Ai≤100000输入样例:
5 2 1 2 3 4 5
输出样例:
6
首先想到暴力,但是时间复杂度O(n^3),需要优化
for(int r = 1; r <= n; r ++) for(int l = 1; l <= r; l ++) { int sum = 0; for(int i = l; i <= r; i ++) sum += a[i]; if(sum % k == 0) ans ++; }
然后想到用前缀和优化一下,时间复杂度O(n^2),还是比较高,过不了所有
for(int r = 1; r <= n; r ++) for(int l = 1; l <= r; l ++) { int sum = s[r] - s[l - 1]; if(sum % k == 0) ans ++; }
因此我们思考
第二层循环的作用是枚举左端点,即(s[r] - s[0, r - 1]) % k = 0,当这个条件成立答案就加一
化简这个式子得到 s[r] % k = s[l − 1] % k
然后我们用空间换时间,开一个cnt[ i ] 数组,表示余数 为 i 的数有几个
用 cnt[ s[i] %k ] 得到前面有无和该前缀和余数相同的前缀和,相同res++
然后注意,余数为0已经满足条件,没有 0 % k == 0 这项,所以要先给 cnt[0] 赋 1
ps:看不懂可以拿样例一点点模拟一遍就能够明白这个过程了(我就是这么懂的)
#include <iostream> #include <algorithm> #include <string> #include <cstdio> using namespace std; typedef long long ll; const int N = 100010; int n, k; ll s[N],cnt[N]; int main() { scanf("%d%d",&n,&k); for(int i = 1;i <= n; i ++) { scanf("%lld",&s[i]); s[i] += s[i - 1]; } ll res = 0; cnt[0] = 1; for(int i = 1;i <= n; i ++) { res += cnt[s[i] % k]; cnt[s[i] % k] ++; } printf("%lld\n",res); return 0; }