目录
一.前缀和与差分
1.基础知识
a.一维
前缀和与差分运算为互逆运算,任意一个数组a的前缀和数组的差分数组是它本身。
//为了避免数组越位,下表从1开始用
for (int i = 1; i <= n; ++i) {
s[i] = s[i - 1] + a[i];
}
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
a | 2 | 6 | 8 | 4 | 3 | |
s | 0 | 2 | 8 | 16 | 20 | 23 |
//倒着用for可以不借助两个数组,这里d和a是为了看着清楚,实际可以用一个数组储存。
for (int i = n; i; --i) {
d[i] = a[i] - a[i-1];
}
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
a | 2 | 6 | 8 | 4 | 3 | |
d | 2 | 4 | 2 | -4 | -1 |
性质:现在区修[2,4]加5,我们重新计算下差分数组,看看会得到什么:
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
a | 2 | 11 | 13 | 9 | 3 | |
d | 2 | 9 | 2 | -4 | -6 |
只有d[2]和d[5]变了!而且我们还可以用刚才的方法算出a数组的所有值!
如果我们要区修数组[l,k]加k,只需差分数组d[l]+=k;d[r+1]-=k,多次区间修改后,只需求d数组的前缀和即可得到修改后a数组的值。
总结:差分的性质:对原数组 [L,R] 区间的值统一加上(减去)k,其实就等于对差分数组 d[L]+=k,d[R+1]−=k
可以把区间操作改为单点操作,巨幅减少时间复杂度
b.二维
难点和易错点:边界值
[推荐学习视频] 二维前缀和_哔哩哔哩_bilibili
前缀和:
比如我们有这样一个矩阵a,如下所示:
1 2 4 3
5 1 2 4
6 3 5 9
我们定义一个矩阵sum,其中,那么这个矩阵就是这样的:
1 3 7 10
6 9 15 22
12 18 29 45
公式:
create sum数组:
int data;
for (int i=1; i<=n; i++) {
for (int j=1; j<=m; j++) {
cin >> data;
sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+data;
}
}
By the way,最近在预习概率论与数理统计,发现二维离散型随机变量中的联合分布函数与二维前缀和有异曲同工之妙!
差分:
二维差分的公式
如何从差分矩阵得到原矩阵呢?可以参考下面公式
差分数组应用:如果我们要在左上角是 (x1,y1),右下角是 (x2,y2) 的矩形区间每个值都 +a,如下图所示
在我们要的区间开始位置(x1,y1)处 +c,根据前缀和的性质,那么它影响的就是整个黄色部分,多影响了两个蓝色部分,所以在两个蓝色部分 -c 消除 +c 的影响,而两个蓝色部分重叠的绿色部分多了个 -c 的影响,所以绿色部分 +c 消除影响。所以对应的计算方法如下:
d[x1][y1] += c;//左上角 d[x1][y2+1] -=c; d[x2+1][y1] -=c; d[x2+1][y2+1] += c;//右下角(把多减去的补回来)
———————————————— 二维差分引用链接:二维差分详解与模板题解析-CSDN博客
2.应用
a.尺取:
原理:尺取算法,其实就像一条毛毛虫在爬动,它主要用来求某些条件下的子序列的长度或者运行次数等等,它思维简单,顾名思义,毛毛虫嘛,一点点往前爬,每次将尾部的数去掉,加上下一个数就好了,中间毛毛虫的身子那一段数是可以不变的。
题目:有n盘火锅围成一个圈,第一盘和最后一盘是相连的,每一盘火锅都有一个价值a[i],现在可以吃连续的m盘火锅,小Z想知道他所吃的那连续的m盘火锅的最大价值可以是多少?你能帮帮憨憨的小Z吗。 输入格式第一行数入两个整数n,m(1<=m<=n<=2000000),分别表示火锅的盘数和可以吃的连续的盘数 第二行输入n的数a[i] (1<=a[i]<=100000),分别表示每一盘火锅的价值.输出格式输出一个整数,表示连续m盘火锅的最大价值 5 3 6 1 2 5 3 样例输出 14
#include<iostream>
using namespace std;
const int maxn = 100005;
int a[maxn];
int main(){
long long n, m, sum = 0, ans = 0;
scanf("%lld %lld", &n, &m);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
long long j = m;//j:尺取时的pointer
for (long long w = 0; w < m; w++)//先计算[0,m-1]的总价值
sum = sum + a[w]; ans = sum;
for (int t = 0; t < n;t++) {//尺取移动次数:总数是多少就移动多少次
if (j == n) j = 0;//指向右边界归0
sum = sum + a[j++];//往后移动一次
ans = max(ans, sum);
sum = sum - a[t];//减去最前面的一个
}
printf("%lld\n", ans);
return 0;
}
b.前缀和
[P5638] 【CSGRound2】光骓者的荣耀 - 洛谷 例题1
最容易失手的地方实际是在区间边界的选取
#include<iostream>
#define ll long long
using namespace std;
const int maxn = 1000005;
ll sum[maxn];
int main() {
int n, k;
cin >> n >> k;
if (k >= n - 1) { printf("0\n"); return 0; }//n-1为路段数
for (int i = 1; i < n; ++i) {
ll x;
scanf("%lld", &x);
sum[i] = sum[i - 1] + x;//维护前缀和数组
}
ll cnt = sum[k];//初始化为[1,k]
for (int i = 1; i < n - k; ++i) {
ll temp = sum[i + k ] - sum[i];//注意区间!!!
cnt = max(cnt, temp);//寻找连续距离最大的长度为k的区间
}
cout << sum[n - 1] - cnt << endl;
return 0;
}
c.差分
例题1:黑暗爆炸 - 3043
我们如果要将l,r内的数都+1/-1”只需要:d[l]+=1;d[r+1]-=1。我们最终要达到的结果是:所有a数列的元素都是相等的,自然相邻元素的差就是0。 要将d数列中第二项至第n项全部变为0并使操作次数最少,首先我们将每个负数和每个正数配对执行操作,设d数列中第2至第n项所有正数分别求和得到的值为x,负数分别求和得到的值的绝对值为y,这一步的操作次数即为min{x,y}。此时还剩余和的绝对值为abs(x−y)的数没有变为0, 每次操作我们可以将其与d[1]或d[n+1]配对进行操作,操作次数为abs(x-y)。
【看不懂请回到上文表格进行演算:d[1]可以为任意值,其决定了所有a[i]的大小(a[i]=d[1])】
因为要使操作次数最小,所以配对的"+1"和"-1"一定会被消去,对于最终数列的种类没有影响。
所以只用考虑剩下来的正数(或负数)的消去方法。
对于一个在i位置的"+1"来说,有两种消去方法:
(1)自己跟自己消,即在s[i]再-1。
从效果上看,相当于给a[i]及后面的数都-1。
(2)跟位置1消,即在s[i]处-1。
相当于给[1,i]之间的数-1。
对于这两种消去方法,最终数组的数字大小相差1。
剩下的数共有abs(x-y)个。
所以最终数组的数字大小最多相差abs(x-y),即数字种类有abs(x-y)+1种。
#include<iostream>
#define ll long long
using namespace std;
const int maxn = 1000005;
ll a[maxn], d[maxn];
ll positive = 0, negative = 0;
int main() {
int n; cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
d[i] = a[i] - a[i-1];
if(i!=1){
if(d[i] > 0) positive+=d[i];
else if(d[i] < 0) negative-=d[i];
}
}
ll cnt = abs(positive - negative) + 1;
// 差分数组区间修改时仅对一对正负对,或一个元素与d[0]/d[n+1]配对,进行增加/减少
cout<<max(positive,negative)<<endl<<cnt<<endl;
return 0;
}
[例题2:p3717] [AHOI2017初中组] cover - 洛谷
这道题实际为一维差分
我们使用差分的思想,对能够覆盖到的地方进行差分 最后看一下哪个位置值为1,即为覆盖的点 我们枚举lenY(纵坐标距离圆心的距离) 通过lenX^2+lenY^2=r^2计算lenX即可,然后进行差分
#include<iostream>
using namespace std;
int n, m, r, map[5001][5001], ans=0;
int main(){
scanf("%d%d%d", &n, &m, &r);
for (int i = 1; i <= m; i++){
int x, y;
scanf("%d%d", &x, &y);
for (int j = max(1, y - r); j <= min(n, y + r); j++){//Traverse the probable value of y
int len1 = y - j;//the value of up to end
int len2 = sqrt((double)(r * r) - len1*len1);//Get the value of left to right
int x1 = max(1, x - len2), x2 = min(n, x + len2);//the left and right edge of x
map[j][x1] += 1, map[j][x2 + 1] -= 1;//Update difference array
}
}
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
map[i][j] += map[i][j - 1];//Recover the former array(The reverse of create pre_sum array)
if (map[i][j]) ans++;
}
}
printf("%d\n",ans);
return 0;
}
d.二维前缀和
例题1:一个非常简单的模板题,请参考以下链接射命丸文 - Problem - MYOJ。
特别说明:i,j均属于[1,n]
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+2;
const int MAXM = 1e3+2;
int sum[MAXN][MAXM] = {};
int main() {
int n,m,r,c;
cin>>n>>m>>r>>c;
int data;
for (int i=1; i<=n; i++) {
for (int j=1; j<=m; j++) {
cin >> data;
sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+data;//Create sum array
}
}
int ans = 0;
for (int i=r; i<=n; i++) {
for (int j=c; j<=m; j++) {
data = sum[i][j]-sum[i-r][j]-sum[i][j-c]+sum[i-r][j-c];//Update sum array
ans = max(ans, data);
}
}
cout << ans << endl;
return 0;
}
例题2:黑暗爆炸 - 1218
到处都是坑(边界值,n的含义区别于平常题)
#include<iostream>
using namespace std;
const int maxn = 5005;//只开5001不够
short map[maxn][maxn];//<=32767因此short就够了
int sum[maxn][maxn];
int main() {
int n, r, x, y, v;
cin >> n >> r;
int row = r, col = r;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &x, &y, &v);
x++; y++;
map[x][y]=v;
row = max(row, x);
col = max(col, y);
}
for(int i=1;i<=row;i++)
for (int j = 1; j <= col; j++) {
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + map[i][j];//Create difference array
}
int data, ans=0;
for (int i = r; i <= row; i++) {
for (int j = r; j <= col; j++) {
data = sum[i][j] - sum[i - r][j] - sum[i][j - r] + sum[i - r][j - r];//Update sum array
ans = max(ans, data);
}
}
printf("%d\n", ans);
return 0;
}
e.二维差分
例题1:模板题二维差分模板——区间修改 - Problem - MYOJ
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+6;
const int MAXM = 1e3+6;
int a[MAXN][MAXM] = {};
int diff[MAXN][MAXM] = {};
int main() {
int n,m,q;
scanf("%d%d%d", &n, &m, &q);
int i, j;
for (i=1; i<=n; i++) {
for (j=1; j<=m; j++) {
scanf("%d", &a[i][j]);
diff[i][j] = a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];//Create difference array
}
}
for (i=0; i<q; i++) {
int x1, y1, x2, y2, c;
scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
//Update difference array
diff[x1][y1] += c;
diff[x1][y2+1] -=c;
diff[x2+1][y1] -=c;
diff[x2+1][y2+1] += c;
}
for (i=1; i<=n; i++) {
for (j=1; j<=m; j++) {
diff[i][j] += diff[i-1][j]+diff[i][j-1]-diff[i-1][j-1];//Recover former array
printf("%d ", diff[i][j]);
}
printf("\n");
}
return 0;
}
二.莫队
最简单的莫队算法用于解决一类序列上无修改只查询的区间问题。经过不同改进后,还可以解决树上路径查询问题,带修改的区间查询问题……总之,莫队算法可以解决一切区间问题。当然,莫队算法还有一个显著特征——莫队算法是一个离线算法。(最后给出所有输出)
1.基础知识
共5个步骤,除了需要自己写Add()与Sub()外均为以下模板
例题1:SPOJ - DQUERY
a.分块
我们将询问先储存下来,再按照某种方法排序,让他减少挪动的次数,这样会变快一些。这种方法是强行离线,然后排序,所以这样的普通莫队不能资瓷修改
那么怎么排序呢?一种解决方式是优先按照左端点排序。
这种排序的方式,保证左端点只会向右挪,但是右端点每次最坏还是可以从最前面挪到最后面,从最后面挪到最前面,这样的复杂度还是 O(nm) ,是不行的。我们的排序是要使左右端点挪动的次数尽量少,所以这里就有一种排序方法:
将序列分成根号n个长度为根号n的块,若左端点在同一个块内,则按右端点排序(以左端点所在块为第一关键字,右端点为第二关键字)
int a[maxn];
int pos[maxn];//记录a数组的元素分块之后的位置
int n,m;//n:数组大小 m:询问次数
int size=sqrt(n);//把a数组分为size份
for(int i=1;i<=n;i++){
cin>>a[i];
pos[i]=i/size;
}
b.询问
struct Q{
int l,r;//区间
int k;//在题目里是第k次询问
}q[maxn];
for(int i=0;i<m;i++){//m次询问
cin>>q[i].l>>q[i].r;
q[i].k=i;//第i次询问
}
c.排序
用到了lambda表达式,相当于直接写排序准则
sort(q, q + m, [](Q x, Q y) {
if (pos[x.l] == pos[y.l]) return x.r < y.r;
else return pos[x.l] < pos[y.l];
});//注意语法,右小括号+结尾分号属于sort函数
d.移动操作
每道题重点在于写Add与Sub函数
int ans[maxn];
int res;
int l=1;r=0;
for(int i=0;i<m;i++){
//技巧1:l,r处于q[l,r]之间时是前缀加减(或者Add()是前缀加减)
//技巧2:始终要让l,r靠近q[l,r],以此得出该加还是该减
while(q[i].l<l) Add(--l);
while(q[i].r>r) Add(++r);
while(q[i].l>l) Sub(l++);
while(q[i].r<r) Sub(r--);
//记录答案
ans[q[i].k]=res;
}
void Add(int n) {//把n位置的数字加入进来
cnt[a[n]]++;
if (cnt[a[n]] == 1) res++;
}
void Sub(int n) {
cnt[a[x]]--;
if (cnt[a[n]] == 0) res--;
}
e.输出ans数组
for(int i=0;i<m;i++){
printf("%d\n",ans[i]);
}
完整代码
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1e6 + 5;
int a[maxn], cnt[maxn], pos[maxn];
long long res = 0, ans[maxn];
struct Q {
int l, r, k;
}q[maxn];
void Add(int n) {
cnt[a[n]]++;//记录a[n]出现的次数
if (cnt[a[n]]==1)res++;//说明加之前不存在这个数
}
void Sub(int n) {
cnt[a[n]]--;
if (cnt[a[n]]==0) res--;//说明减之前这个数只有一个
}
int main() {
int n,m;
cin >> n ;
int size = sqrt(n);
for (int i = 1; i <= n; i++) {//这里由于后面的询问区间是1-n,所以从1开始写入
scanf("%d", &a[i]);
pos[i] = i / size;
}
cin >> m;
for (int i = 0; i < m; i++) {
scanf("%d%d", &q[i].l, &q[i].r);
q[i].k = i;
}
sort(q, q + m, [](Q x, Q y) {
if (pos[x.l] == pos[y.l]) return x.r < y.r;
else return pos[x.l] < pos[y.l];
});//注意语法,右小括号+结尾分号属于sort函数
int l = 1, r = 0;//注意l比r大
for (int i = 0; i < m; i++) {
while (q[i].l < l) Add(--l);
while (q[i].l > l) Sub(l++);
while (q[i].r < r) Sub(r--);
while (q[i].r > r) Add(++r);
ans[q[i].k] = res;
}
for (int i = 0; i < m; i++) {
printf("%lld\n", ans[i]);
}
return 0;
}
2.应用
例题:小B的询问小B的询问 - 洛谷
出错点:
1.while()四个分支的加减,上文已有,不再赘述
2.数字1和字母l分不清楚,低级错误
3.读入a[i]的for循环要从1开始,原因是区间询问的开始下标是1
4.ans用于保存res,两个都要开long long
5.sort lambda表达式的写法
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5e4 + 5;
int a[maxn], cnt[maxn], pos[maxn];
long long res = 0, ans[maxn];
struct Q {
int l, r, k;
}q[maxn];
void Add(int n) {
cnt[a[n]]++;//记录a[n]出现的次数
res += (cnt[a[n]] * cnt[a[n]] - (cnt[a[n]] - 1) * (cnt[a[n]] - 1));//res的变化量=cnt平方的变化量
}
void Sub(int n) {
cnt[a[n]]--;
res -= ((cnt[a[n]] + 1) * (cnt[a[n]] + 1) - cnt[a[n]] * cnt[a[n]]);
}
int main() {
int n, m, k;
cin >> n >> m >> k;
int size = sqrt(n);
for (int i = 1; i <= n; i++) {//这里由于后面的询问区间是1-n,所以从1开始写入
scanf("%d", &a[i]);
pos[i] = i / size;
}
for (int i = 0; i <m; i++) {
scanf("%d%d", &q[i].l, &q[i].r);
q[i].k = i;
}
sort(q, q + m, [](Q x, Q y) {
if (pos[x.l] == pos[y.l]) return x.r < y.r;
else return pos[x.l] < pos[y.l];
});//注意语法,右小括号+结尾分号属于sort函数
int l = 1, r = 0;//注意l比r大
for (int i = 0; i < m; i++) {
while (q[i].l < l) Add(--l);
while (q[i].l > l) Sub(l++);
while (q[i].r < r) Sub(r--);
while (q[i].r > r) Add(++r);
ans[q[i].k] = res;
}
for (int i = 0; i < m; i++) {
printf("%lld\n", ans[i]);
}
return 0;
}