一.前缀和
1.一维前缀和
一维前缀和可以类比数学中的数列求和。
a1 a2 a3 a4 ... an // 数组a
s1 s2 s3 s4 ... sn // 前缀和数组
s1 = s0 + a1;
s2 = s1 + a2;
s3 = s2 + a3;
s4 = s3 = a4;
......
sn = sn-1 + an;
假设有数组a,求其前缀和数组s.其推导如上图。
一维前缀和公式就为
sn = sn-1 + an;
要求任意a[l] 到 a[r] 之间(包括两端的数)所有数的和就为
s[l,r] = s[r+1] - s[l];
杂谈:
对于任意一组数,假如我们只求一次前缀和,我们完全可以不开数组,用数组的意义在于,多次求和,这会剩下运算时间。
令人兴奋的是,我在这里看出用数组来储存对应的前缀和有一定的哲学意味,就好比我们做一些物理题会有二级结论,我们会首先推一遍,得出二级结论,然后记住这个二级结论。这里计算机也好像有了“记忆”,经过“推导”,记住了一些“二级结论”;但有利也有弊,我们记住了一些物理二级结论后,做题的速度虽然快了,但我们记得多了,脑的负担也增加了,计算机也是如此,虽然算的快了,但储存空间增大了。
说到底前缀和数组,是用空间换时间.
2.二位前缀和
对于二维前缀和,我们可以把数组抽象抽象成如上图所示。
如图,一个直角坐标系的正正区域,每隔一个“1”距离,画一条线,构成了边长为“1”的正方形小格子,每个小格子存放着该正方形右下角坐标对应的数。这样,我们再求前缀和,就是数学几何问题了,而且是小学那种。
现在假设有一组矩形数组,要求对应的前缀和数组
//一个矩形数组
a[1,1] a[1,2] ... a[1,n]
a[2,1] a[2,2] ... a[2,n]
.
.
.
a[n,1] a[n,2] ... a[n,n]
//对应的前缀和数组
s[1,1] s[1,2] ... a[1,n]
s[2,1] s[2,1] ... s[2,n]
.
.
.
s[n,1] s[n,1] ... s[n,n]
要求任意的s[i][j],就是求从原点到(i,j)围成的方格中所有数的和,可以等价于求该图形的面积(注意面积的最小单位是1*1)这时,图中划分了四个区域,红区,蓝区,红白区,黑区。整个图形的面积就是 s = 红 + 蓝 - 红蓝 + 黑。
刚学的时候,我有过一个比较蠢的想法,为什么又加又减的,直接从原点加到(i,j)不行了吗?当然是不可以的。比如在一维中,要算s[n],你得先算s[n-1]。这就是为什么,又加又减的。
最后得出前缀和公式是
s[i,j] = s[i-1,j] + s[i,j-1] + s[i-1,j-1] + a[i,j];
假如我们要求任意的矩形[x1,y1]-[x2,y2]的前缀和,就是
sum = s[x2][y2] - s[x1-1][y1] - s[x1][y1-1] + s[x1-1][y1-1];
二.差分
1.一维差分
差分可以说是前缀和的逆运算。
b1 b2 b3 ... bn //差分数组
a1 a2 a3 ... an //原数组
b1 = a1 - a0;
b2 = a2 - a1;
b3 = a3 - a2;
...
bn = an - an-1;
假设有一个差分数组b,原数组a就是对应的前缀和。
所以对于给定的数组a,其对应有差分数组b。
bn = an - an-1;
利用差分数组与对应的原数组的关系,我们如果要对原数组更改,可以改变对应的差分数组来改变原数组。
扯了一堆,主要应用这一个问题,即我们给出一个原数组a,又让原数组的区间[l,r]里每个数都加上或减去一个常数c,那么我们可以让差分数组里的数b[l]+c,这样的效果是,a[l]以后的数都加了c,但我们只要区间[l,r]里的原数组加上c,所以我们让b[r+1]-c;
//让[2,4]区间里原数组a都加c
b1 b2 b3 b4 b5 b6
+c +c +c +c +c
-c -c
a1 a2 a3 a4 a5 a6
+c +c +c
所以这里对差分数组b进行的操作就是
b[l] += c; b[r+1] += c;
然后对该区间的差分数组求和,记得改变后的原数组。
2.二维差分
二维数组主要仍是针对一维差分中提到的问题。
即给出一个矩阵原数组,让[x1,y1]-[x2,y2]矩阵原数组都加上常数c(其实是加或减,不妨认为是加)
解决该问题,我们大体分为三步:(1)构造原数组对应的差分数组。(2)对差分数组进行操作。(3)对差分数组求和。
1.我们先来看对二维差分数组进行操作。
如图,把上图分为四个区域:黄区(有黄颜色的区域),红黄区,蓝黄区,红黄蓝区。(图可能不好分辨,尽力了。。。)现在我们让b[x1][y1]+c,其效果是黄区所有a数组的数都加了c;为了限制区间,我们让 b[x2+1][y1]-c,b[x1][y2+1]-c,效果是红黄区和蓝黄区原数组的数都正常了,但红黄蓝区就多减了c,所以我们让b[x2+1][y2+1],这样就全部操作就完成了。
其代码为
void insert(int x1,int y1,int x2,int y2,int c)
{
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;
}
2.然后我们看一下二维数组的构造这一步。
这里有两种方法,一是利用前缀和公式逆运算得到差分数组
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1];
这种方法较为容易理解。
二是利用差分的逆运算来得到差分数组
经过差分数组的操作那一步,我们可以知道,差分的效果就是得到这样一个数组:对其指定区间[x1,y1]-[x2,y2]的数组求前缀和,那么原数组a,都加了一个c,也就是说,对应区间的每一个数,加到其对应的坐标,该数多了一个c。
我们要构造的差分数组应满足这样的性质,即从b[1][1]开始加一直加到b[i][j],应有b[i][j] == a[i][j]。
所以我们现在建立一个数组b[N][N],这里边的数都为零,对其进行操作,操作的范围是每一个原数组的数的下标[i,j]-[i,j],常数c是a[i][j],这样就得到了要构造的差分数组。
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
insert(i,j,i,j,a[i][j]);
3.最后就是第三步,对处理好的差分数组求和。