良心之作,史上最全前缀和与差分!!!

总的来说

1.前缀和:对一个序列进行O(N)预处理(或者一个矩阵,即二维前缀和)后,O(1)地查询任意一段子序列的和.

2.差分:用于求解多次区间修改与区间询问的题型,例如多次给[ L , R ] 内所有数 + val,就可以用差分以及前缀和来优化。区间操作O(1),区间询问O(N)处理,O(1)查询.

3.树上差分:同样的,如果我们有若干次操作与若干次询问,每次操作对从u 到 v 路径上所有节点加一个 值,那么我们用树上差分可以将时间复杂度控制在O(1)上,询问同样是O(N)处理,O(1)查询.

注意:差分为离线算法,不支持同时修改同时询问操作,算法复杂度会退化

1.前缀和

1.1 问题描述:给出一串长度为 n 的数列a1,a2,a3…an,再给出m个询问,每次询问给出L,R两个数,要求给出区间[L,R]里的数的和。
1.2基本思路:朴素算法,将[L,R]中每个数依此累加,复杂度为O(MN);
前缀和的思路:开一个数组d[];
d[i] = a[i] + a[i-1] + a[i-2] + ... + a[1];
记录序列从开始至第i位置的总和,由此我们可以得出任意一段序列[1,x]的和,即[1,L-1],[1,R],ans = d[R] - d[L - 1],复杂度为O(1);预处理操作复杂度为O(N);

3.一维差分

3.1 问题描述:给出一串长度为 n 的数列a1,a2,a3…an , 给出k次操作,每次操作给出L、R、val,要求对 [L , R]内所有元素加上val,再给出m个询问,每次询问给出L,R两个数,要求给出区间[L,R]里的数的和。
3.2 朴素思路:很容易想到的做法就是每次给出L,R 和 val后,我们就挨个对[L,R] 内所有元素+val,再在 询问时将答案挨个加起来。这样时间复杂度为O(kmn)
3.3 差分思路:顺应着前缀和,假如我们将所有修改操作进行一次前缀和,在所有修改操作完成后,进行一次总的修改,即大大降低时间复杂度;
3.4 一维差分实现思路:开一个c[]数组,用来记录修改的前缀和,假设我们对区间[L,R],进行修改,我们只需令c[L] += val,c[R+1] -= val,将所有修改操作类此标记,在最后对c[]数组进行一次前缀和,将原先处理好的d[]前缀和数组,按位进行加法,即得最终修改值;
c[L] += val;c[R+1] -= val;
一维差分前缀和
c[L] = val;c[L+1] = val;......c[R] = val;c[R+1] = 0;//为什么?因为我们已经将c[R+1]标记为了 -val;
修改数组d[];
for(i,1...n)
    d[i] += c[i];

2.二维前缀和

2.1 公式:
d[i][j] = Σ(a[i...0][j...0]);
2.2 基本问题:给定一个n*m大小的矩阵a,有q次询问,每次询问给定x1,y1,x2,y2四个数,求以(x1,y1)为左上角坐标和(x2,y2)为右下角坐标的子矩阵的所有元素和。注意仍然包含左上角和右下角的元素。
二维前缀和实现思路:d[i][j]记录以i,j为右下角下标,以1,1为左上角下标的矩阵内所有数据域之和;实现询问操作:
图解

良心之作,史上最全前缀和与差分!!!_第1张图片

红色矩阵a,黄色为目标矩阵;ans = (黄,灰,蓝,紫四个矩阵) - (灰,蓝两个矩阵) - (蓝,紫两个矩阵)+(蓝色矩阵);这里我们以蓝色矩阵为过渡,O(1)时间内求解完毕;

Talk is cheap.

预处理操作

//d[i][j]应预先存好第(i,j)位置的元素值;
for(int i=1;i<=n;i++){
	for(int j=1;j<=m;j++)
	d[i][j] += d[i][j-1] + d[i-1][j] - d[i-1][j-1];//类似于询问操作
} 

询问操作

ans = d[x2][y2] - d[x1-1][y2] - d[x2][y1-1] + d[x1-1][y1-1];//x1 - 1和y1 - 1是因为题目要包含左上角和右下角的元素;

4.二维差分

思路引进:既然一维前缀和能差分,二维同样可以;(假的证明)
实质上是只不过是把差分从一维引进到二维而已;
同样开一个c[][]数组来维护修改操作;
for(int i  = 1;i <= m;i++){//m为修改次数;
int x1,y1,x2,y2,p;
cin>>x1>>y1>>x2>>y2>>p;
c[x1][y1] += p;c[x2+1][y2+1] += p;
c[x2+1][y1] -= p;c[x1][y2+1] -= p;//与一维差分相同,都需在后一位维护一个-p;
}
后续操作均与一维差分类似,在此不再赘述;

5.树上差分

5.1 问题描述:
· 给定一棵有N个点的树,所有节点的权值初始时都为0。
· 有K次操作,每次指定两个点u , v,将 u 到 v 路径上所有点的权值都+1。
· 请输出K次操作完毕后权值最大的那个点的权值。
5.2 朴素思路:不用多想,最暴力的做法就是我们找到 u 到 v 路径上的所有点并+1(可以利用 LCA )。最后,再遍历所有点查找权值最大的点并输出。这样时间复杂度为O(KN),这还不包括 LCA 查找路径的时间。非常暴力
5.3 求u到v的路径:那么我们知道,如果假设我们要考虑的是从 u 到 v 的路径,u 与 v 的 lca 是 a ,那么 很明显,假如路径中有一点 u′ 已经被访问了,且 u′ ≠ a ,那么 u’ 的父亲也一定会被访问。(为什么呢?因为这是一棵树)所以,我们可 以将路径拆分成两条链,u -> a 和 a -> v。
图解

良心之作,史上最全前缀和与差分!!!_第2张图片

(图糙,轻喷)

a是最近公共祖先,从 u -> v,必然要经过u的父亲u’,但并非要经过a的父亲,由此类推(由上可知证毕
5.4树上差分:常见的树上差分有两种形式,即
· 关于边的差分
· 关于点的差分。
5.4.1 关于边的差分:
将边拆成两条链之后,我们便可以像差分一样来找到路径了。用 cf[ i ] 代表从i到i的父亲这一条路 径经过的次数。因为关于边的差分,a 是不在其中的,所以考虑链 u -> a,则就要使cf[ u ]++,cf[ a ]− −。然后链a -> v,也是cf[ v ]++,cf[ a ]−−。所以合起来便是cf[ u ]++,cf[ v ]++,cf[ a ]−=2。然后, 从根节点,对于每一个节点x,都有如下的步骤:
(1)枚举x的所有子节点u
(2)dfs所有子节点u
(3)cf[ x ] + = cf[ u ]
那么,为什么能够保证这样所有的边都能够遍历到呢?因为我们刚刚已经说了,如果路径中有一点u′已 经被访问了,且u′≠a,那么u′的父亲也一定会被访问。所以u′被访问几次,它的父亲也就因为u′被访问了几 次。所以就能够找出所有被访问的边与访问的次数了。路径求交等一系列问题就是通过这个来解决的。因 为每个点都只会遍历一次,所以其时间复杂度为O(n).
5.4.2 关于点的差分:
还是与和边的差分一样,对于所要求的路径,拆分成两条链。步骤也和上面一样,但是也有一些不 同,因为关于点,u与v的lca是需要包括进去的,所以要把lca包括在某一条链中,用cf[ i ] 表示 i 被访问的 次数。最后对 cf 数组的操作便是cf[ u ]++,cf[v]++,cf[ a ]−−,cf[ father[a] ]−−。其时间复杂度也是一 样的O(n).
5.5 树上差分总结:将需要修改的一段路径,拆分成两条链,可以理解作两段序列,然后分别进行一维的差分;

你可能感兴趣的:(前缀和与差分)