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

总的来说

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为左上角下标的矩阵内所有数据域之和;实现询问操作:
图解

理解图

红色矩阵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。
图解

(图糙,轻喷)

(图糙,轻喷)

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 树上差分总结:将需要修改的一段路径,拆分成两条链,可以理解作两段序列,然后分别进行一维的差分;
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前缀和差分是一类常用的算法,它们常常被用来优化一些区间操作的问题,如求区间和、区间最大值/最小值等等。下面我们将分别介绍前缀和差分的定义、用法和常见问题。 ## 前缀和 前缀和,顾名思义,就是把前面所有数的和都求出来,用一个数组存起来,以便之后的查询。 ### 定义 给定一个长度为 $n$ 的序列 $a$,令 $s_i = \sum_{j=1}^{i}a_j$,则 $s$ 称为序列 $a$ 的前缀和数组。 ### 用法 前缀和的主要作用是用 $O(1)$ 的时间复杂度求出一个区间 $[l,r]$ 的和,即 $s_r - s_{l-1}$。这是因为 $s_r$ 存储了序列从 $1$ 到 $r$ 的和,而 $s_{l-1}$ 存储了序列从 $1$ 到 $l-1$ 的和,因此区间 $[l,r]$ 的和可以通过两个前缀和相减计算得出。 前缀和的时间复杂度为 $O(n)$,因为需要遍历一遍序列求出前缀和数组。但是,如果有多个查询需要求区间和,那么使用前缀和可以将每次查询的时间复杂度降低到 $O(1)$。 ### 代码实现 下面是使用前缀和求区间和的代码实现: ```cpp vector<int> a; // 原序列 vector<int> s(a.size() + 1); // 前缀和数组 // 计算前缀和 for (int i = 1; i <= a.size(); i++) { s[i] = s[i - 1] + a[i - 1]; } // 查询区间 [l, r] 的和 int sum = s[r] - s[l - 1]; ``` ## 差分 差分前缀和相反,它主要用来对区间进行修改。我们可以利用差分数组进行区间修改,并最终得到修改后的序列。 ### 定义 给定一个长度为 $n$ 的序列 $a$,令 $d_i = a_i - a_{i-1}$($d_1 = a_1$),则 $d$ 称为序列 $a$ 的差分数组。 ### 用法 差分的主要作用是对区间进行修改。假设我们需要将区间 $[l,r]$ 的数加上 $k$,我们可以将差分数组的 $d_l$ 加上 $k$,将 $d_{r+1}$ 减去 $k$。这样,对差分数组求前缀和,就可以得到修改后的序列。 具体来说,我们可以按照以下步骤进行区间修改: 1. 对差分数组的 $d_l$ 加上 $k$; 2. 对差分数组的 $d_{r+1}$ 减去 $k$; 3. 对差分数组求前缀和,得到修改后的序列。 差分的时间复杂度为 $O(n)$,因为需要遍历一遍序列求出差分数组。但是,如果有多次区间修改需要进行,那么使用差分可以将每次修改的时间复杂度降低到 $O(1)$。 ### 代码实现 下面是使用差分进行区间修改的代码实现: ```cpp vector<int> a; // 原序列 vector<int> d(a.size() + 1); // 差分数组 // 计算差分数组 for (int i = 1; i < a.size(); i++) { d[i] = a[i] - a[i - 1]; } // 修改区间 [l, r],将数加上 k d[l] += k; d[r + 1] -= k; // 对差分数组求前缀和,得到修改后的序列 for (int i = 1; i < d.size(); i++) { a[i] = a[i - 1] + d[i]; } ``` ## 常见问题 ### 1. 差分数组的长度是多少? 差分数组的长度应该比原序列长度多 1,因为 $d_1 = a_1$。 ### 2. 什么情况下使用前缀和?什么情况下使用差分? 如果需要进行多次区间查询,那么使用前缀和可以将每次查询的时间复杂度降低到 $O(1)$;如果需要进行多次区间修改,那么使用差分可以将每次修改的时间复杂度降低到 $O(1)$。 ### 3. 前缀和差分的本质区别是什么? 前缀和差分都是用来优化区间操作的算法,它们的本质区别在于: - 前缀和是通过预处理前缀和数组来优化区间查询; - 差分是通过预处理差分数组来优化区间修改。 ### 4. 前缀和差分能否同时使用? 当然可以。如果需要同时进行区间查询和修改,我们可以先使用差分数组对区间进行修改,然后再对差分数组求前缀和,得到修改后的序列。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值