目录
引入
对于一组数据,假设10个数为
9 3 4 2 6 5 8 7 10 1
要求我们求出第1个到第3个数的和,我们很容易想到以下代码
int sum=0;
for(int i=1;i<=3;i++) {
sum+=a[i];
}
cout<<sum<<endl;
如果再次要求,求出第1个数到第8个数之间的和,那简单嘛,再遍历一次。
int sum=0;
for(int i=1;i<=8;i++) {
sum+=a[i];
}
cout<<sum<<endl;
如果再再要求,求出第4个数到第8个数之间的和,那...再遍历嘛
int sum=0;
for(int i=4;i<=8;i++) {
sum+=a[i];
}
cout<<sum<<endl;
如果再再再要求,求出第2个数到第6个数之间的和,那..那...再遍历嘛
如果.........
于是这种就是典型的区间和问题。通常的代码是这样的:
int m;cin>>m;
while(m--) {
int l,r,sum=0;
cin>>l>>r;
for(int i=l,i<=r;i++) {
sum+=a[i];
}
cout<<sum<<endl;
}
打住打住,我们之前不是已经遍历过1~3和1~8了吗,仔细想一想,4~8之间的和,是不是就是1~8的和减去1~3的和?
所以可不可以像记忆化递归那样?进行一个记忆储存?把1~3的值储存下来,把1~8的值储存下来,然后直接相减,岂不美哉?
那答案想必都知道,肯定可以的,这就是前缀和算法。
前缀和
前缀和例题:
前缀和算法是基于递推的衍生算法,建以先把递推搞懂。
这里有两个数列
9 3 4 2 6 5 8 7 10 1 (这个是刚才例子的数列)
a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10]
9 12 16 18 24 29 37 44 54 55
b[1] b[2] b[3] b[4] b[5] b[6] b[7] b[8] b[9] b[10]
仔细观察观察,下面的b数组,对应的b[ i ]的值,就是a[1]+a[2]+....+a[ i ] 的和。像这样,我们就完成了数组求和的储存。
所以求解a[4]+a[5]+a[6]+a[7]+a[8] 也就等于 b[8]-b[3]=44-16=2+6+5+8+7=28。
利用递推的思想,我们不难发现其实b[ i ]还等于 b[ i -1 ]+a[ i ]。
但由于有时候原数组a[n],最初的值意义已经不大,所以可以完全覆盖,也就是说直接前缀求和修改a数组
for(int i=2;i<=n;i++) {
a[i]+=a[i-1];
}//前缀求和
//这时候a[n]的值就是原数组的和
此时a数组,就完全与上面例子中的b数组一样了。
for(int i=n-1;i>=1;i--) {
a[i]+=a[i+1];
}//像这样,从后往前加,就是后缀求和
//这时候a[1]的值就是原数组的和
差分
差分例题:
差分也就是前缀和的逆运算
还是刚才的例子
9 3 4 2 6 5 8 7 10 1
a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10]
9 12 16 18 24 29 37 44 54 55
b[1] b[2] b[3] b[4] b[5] b[6] b[7] b[8] b[9] b[10]
从a数组到b数组,就是前缀和, 那么从b数组到a数组,就是差分了。
for(int i=n;i>=2;i--) {
a[i]-=a[i-1];
}//差分代码
差分的意义就是区间修改,让原数组的第 i 项到第 j 项都加某个数,或者都减某个数。
例如:
1 2 2 4 3 1 2
a[1] a[2] a[3] a[4] a[5] a[6] a[7]
差分一下(当然,可以再前缀和回到原数组)
1 1 0 2 -1 -2 1
b[1] b[2] b[3] b[4] b[5] b[6] b[7]
如果说让原数组第2项到第5项都加上2。
1 4 4 6 5 1 2
a[1] a[2] a[3] a[4] a[5] a[6] a[7]
那么再将该数组差分一下
1 3 0 2 -1 -4 1
b[1] b[2] b[3] b[4] b[5] b[6] b[7]
再对比一下原来的差分数组:我们可以发现差分后,b[2]加了2,b[6]减去了2。
因此,对于让原数组的第 i 项到第 j 项都加某个数,其实可以通过差分,让差分数组第 i 项加上该数,让差分数组第 j+1 项减去该数,即可达到目的。
同理,对于让原数组的第 i 项到第 j 项都减某个数,让差分数组第 i 项减去该数,让差分数组第 j+1 项加上该数,即可达到目的。