前缀和
前缀和是快速求取静态数组内某一区间内所有数的和的一种方法。如果在求取的过程中数组发生变动,则可以采用树状数组、线段树等方法。
例题1 一维前缀和
输入一个长度为n的整数序列。
接下来再输入m个询问,每个询问输入一对l, r。
对于每个询问,输出原序列中从第l个数到第r个数的和。
输入格式
第一行包含两个整数n和m。
第二行包含n个整数,表示整数数列。
接下来m行,每行包含两个整数l和r,表示一个询问的区间范围。
输出格式
共m行,每行输出一个询问的结果。
数据范围
(1≤l≤r≤n)
(1≤n,m≤100000)
(−1000≤数列中元素的值≤1000)
#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int s[N];
int a[N];
int main(void)
{
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
",s[r] - s[l-1]);
}
return 0;
}
例题2 二维前缀和
输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数n,m,q。
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。
输出格式
共q行,每行输出一个询问的结果。
数据范围
(1≤n,m≤1000)
(1≤q≤200000)
(1≤x1≤x2≤n)
(−1000≤矩阵内元素的值≤1000)
#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N][N];
int s[N][N];
int n,m,q;
int main(void)
{
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,x2,y1,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
printf("%d
",s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
}
return 0;
}
练习1 K倍区间
给定一个长度为N的数列,如果其中一段连续的子序列之和是K的倍数,我们就称这个区间[i,j]是K倍区间。
你能求出数列中总共有多少个K倍区间吗?
数据范围
(1≤N,K≤100000)
(1≤Ai≤100000)
思路:
- 通过二重循环控制子序列的起始和终止位置,遍历求每个每个子序列的和(S[r] - S[l-1] % k)是否等于0,时间复杂度是(O(n^2)),大约(10^9)量级,所以需要考虑降低时间复杂度。
2.降低时间复杂度的一般方式是用空间换时间. - S[r] - S[l-1] % k == 0 等价于 S[r] % k == S[l-1] % k,所以可以声明一个数组用来保存余数为(s[i] % k)的个数,这样只需要一层循环即可.
4.注意数据溢出的情况
代码:
#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n,k;
LL s[N],cnt[N];
int main(void)
{
scanf("%d %d",&n,&k);
//计算前缀和
for(int i = 1; i <= n;i++){
scanf("%lld",&s[i]); //注意LL的读取
s[i] += s[i-1];
}
LL ans = 0; //注意答案可能溢出
for(int i = 0;i <= n;i++) //考虑和为0也也是k倍区间,所以从0开始遍历
{
//假设已经有两个S[i]的余数为x,则又有一个S[i]%k == x时,会产生两个新的答案,所以有下面两句
ans += cnt[s[i] % k];
cnt[s[i] % k]++;
}
printf("%lld",ans);
return 0;
}
练习2 -激光炸弹
思路:
这也是一个典型的二维前缀和的问题,大致方向很好想,但是一些处理细节还是很有技巧的。
需要注意一点:对于一个(R×R)的炸弹,最多只能炸到((R)×(R))个格点,因为边界是炸不到的:
代码1:
#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5010;
int s[N][N];
int n,r;
int ans;
int main(void)
{
scanf("%d%d",&n,&r);
int x,y,w;
int xr = 0,yr = 0,xl = 1e5,yl = 1e5;
for(int i = 0;i < n;i++)
{
scanf("%d%d%d",&x,&y,&w);
xr = max(xr,x+1); yr = max(yr,y+1);
xl = min(xl,x+1); yl = min(yl,y+1);
s[x+1][y+1] = w;
}
for(int i = 1; i <= xr;i++)
for(int j = 1;j <= yr;j++)
s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
int chang = xr - xl+ 1, kuan = yr - yl + 1;
if(chang < r && kuan >= r)
{
int x1 = xl,x2 = xr;
for(int y1 = yl,y2 = y1+r-1;y2 <= yr; y1++,y2++ )
ans = max(ans, s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] );
}
else if(chang >= r && kuan < r)
{
int y1 = yl,y2 = yr;
for(int x1 = xl,x2 = x1+r-1;x2 <= xr; x1++ ,x2++)
ans = max(ans, s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] );
}
else if(chang < r && kuan < r)
ans = s[xr][yr];
else
{
for(int x1 = xl,x2 = x1+r-1; x2 <= xr;x1++, x2++)
for(int y1 = yl,y2 = y1+r-1; y2 <= yr; y1++, y2++){
int tmp = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
ans = max(ans, tmp );
}
}
printf("%d",ans);
return 0;
}
这一份代码冗余在:1.对边长R和阵地长宽进行分类讨论(其实没有必要). 2.使用了两个坐标来计算,不如使用一个坐标 + 边长R的方式好。
代码2 -重构:
#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5010;
int s[N][N];
int n,r;
int ans;
int main(void)
{
scanf("%d%d",&n,&r);
//阵地面积最多5000 * 5000
r = min(5001,r);
int xr = r,yr = r;
for(int i = 0;i < n;i++)
{
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
xr = max(xr,x+1); yr = max(yr,y+1);
s[x+1][y+1] = w;
}
//求前缀和
for(int i = 1; i <= xr;i++)
for(int j = 1;j <= yr;j++)
s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
//枚举右下角坐标(i,j)则子矩阵(i-r+1,j-r+1) (i,j)的和计算如下:
for(int i = r;i <= xr;i++)
for(int j = r;j <= yr;j++)
ans = max(ans , s[i][j] - s[i-r][j] - s[i][j-r] + s[i-r][j-r]);
printf("%d",ans);
return 0;
}
差分
差分用来简化对原序列的一部分加上同一个数a的操作,如果直接操作原数组,则时间复杂度O(n),但是使用差分则可以降低到O(1);
在需要多次进行上述操作的情况下,使用差分会带来时间上显而易见的好处。
一维差分 -模板题
思路:
- 差分是前缀和的逆运算,也就是说将原数组看作S数组,则差分数组b[i] = s[i] - s[i-1]
- 关键操作:将s[l,r]加上c ,等价于b[l] += c , b[r + 1] -= c
- 原数组s和差分数组b都需要从1开始存储,且差分数组b还需要再后面多留出1位。
代码:
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 100010;
int s[N];
int b[N];
int n,m;
int main(void)
{
cin >> n >> m;
for(int i = 1; i<= n; i++)
{
scanf("%d",&s[i]);
//构造差分数组
b[i] = s[i] - s[i - 1];
}
int l,r,v;
while(m --)
{
scanf("%d%d%d" ,&l , &r ,&v);
b[l] += v, b[r + 1] -= v;
}
//反推原数组
for(int i = 1; i <= n; i++)
{
s[i] = s[i-1] + b[i];
printf("%d ",s[i]);
}
return 0;
}
二维差分
思路:
- b(x,y) = s(x,y) - s(x-1,y) - s(x,y-1) + s(x-1,y-1)
2.对原矩阵(x1,y1),(x2,y2)加上c:
代码:
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 1010;
int s[N][N],b[N][N];
int n, m, q;
int main(void)
{
cin >> n >> m >> q;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
scanf("%d",&s[i][j]);
//构造差分数组
b[i][j] = s[i][j] - s[i-1][j] - s[i][j-1] + s[i-1][j-1];
}
while(q --)
{
int x1,x2,y1,y2,c;
scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
b[x1][y1] += c;
b[x2 + 1][y2 + 1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= c;
}
//生成原数组
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
s[i][j] = b[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1];
printf("%d ",s[i][j]);
}
puts("");
}
return 0;
}
三体攻击 (二分 + 三维差分)
思路:
1.第一艘飞船可能再任意一次攻击之后爆炸,所以可以使用二分,分界条件是刚好有飞船爆炸。
2.记忆三维前缀和:写出0~7的二进制,如果1的个数是奇数,则符号是+,反之符号是-;
3.关键操作:写出0~7的二进制,1对应的位置是右下角坐标+1,如果1的个数是奇数,则符号是+ ,反之符号是-;
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2000010;
LL bp[N], s[N], b[N];
int op[N / 2][7];
int A , B , C ,m;
int d[8][4] = {
{0 , 0 , 0 , 1},
{0 , 0 , 1 , -1},
{0 , 1 , 0 , -1},
{0 , 1 , 1 , 1},
{1 , 0 , 0 , -1},
{1 , 0 , 1 , 1},
{1 , 1 , 0 , 1},
{1 , 1 , 1 , -1}
};
int getIndex(int i ,int j ,int k)
{
return (i * B + j) * C + k;
}
bool check(int mid)
{
memset(s , 0 ,sizeof s);
memcpy(b, bp , sizeof b);
for(int i = 1; i <= mid; i++)
{
int x1 = op[i][0], x2 = op[i][1], y1 = op[i][2] , y2 = op[i][3] , z1 = op[i][4] , z2 = op[i][5], h = op[i][6];
b[getIndex(x1 , y1 , z1)] -= h;
b[getIndex(x1 , y1 , z2 + 1)] += h;
b[getIndex(x1 , y2 + 1 , z1)] += h;
b[getIndex(x1 , y2 + 1 , z2 + 1)] -= h;
b[getIndex(x2 + 1 , y1 , z1)] += h;
b[getIndex(x2 + 1 , y1 , z2 + 1)] -= h;
b[getIndex(x2 + 1 , y2 + 1 , z1)] -= h;
b[getIndex(x2 + 1 , y2 + 1 , z2 + 1)] += h;
}
for(int i = 1; i <= A ; i++)
for(int j = 1; j <= B; j++)
for(int k = 1; k <= C; k++)
{
s[getIndex(i , j , k)] += b[getIndex(i , j , k)];
for(int u = 1; u < 8; u++)
{
int x = i - d[u][0], y = j - d[u][1] , z = k - d[u][2] , t = d[u][3];
s[getIndex(i ,j ,k)] -= s[getIndex(x , y , z)] * t; //不要写成b[]
}
if(s[getIndex(i , j, k)] < 0) return true;
}
return false;
}
int main(void)
{
cin >> A >> B >> C >> m;
for(int i = 1; i <= A; i++)
for(int j = 1; j <= B; j++)
for(int k = 1; k <= C; k++)
scanf("%lld", &s[getIndex(i , j , k)]);
//构造差分数组
for(int i = 1; i <= A; i++)
for(int j = 1;j <= B; j++)
for(int k = 1; k <= C ; k++)
//使用偏移量枚举的方式
for(int u = 0; u < 8; u++)
{
int x = i - d[u][0], y = j - d[u][1] , z = k - d[u][2] , t = d[u][3];
bp[getIndex(i , j , k)] += s[getIndex(x , y , z)] * t;
}
//读取进攻的坐标,从1读取可以和次数对应
for(int i = 1; i <= m; i++)
for(int j = 0; j < 7; j++)
scanf("%d", &op[i][j]);
//二分求刚好有飞船爆炸的分界点
int l = 1 ,r = m;
while(l < r)
{
int mid = l + r >> 1;
if(check(mid))
r = mid;
else
l = mid + 1;
}
cout << r << endl;
return 0;
}