差分与前缀和 差分与前缀和是一对互逆的操作,常常用于处理区间问题,差分法是解决区间加减问题,前缀和是解决区间求和问题的常用办法。
知识点
差分法 差分法的应用主要是用于处理区间问题。当某一个数组要在很多不确定的区间,加上相同的一个数。我们如果每个都进行加法操作的话,那么复杂度 $O(nm)$ 是平方阶的,非常消耗时间。
如果我们采用差分法,将数组拆分,构造出一个新的拆分数组,通过对数组区间的端点进行加减操作,最后将数组和并就能完成原来的操作。
这样处理后,时间复杂度降低为 $O(N)$,虽然感觉操作变得更加复杂了,但是只用对边界操作确实比操作一整个区间的方法要优秀的多。
听到这里也是吊足了胃口,那到底怎么对区间操作呢,请大家跟随我的讲解,慢慢理解。
差分法的特点:
将对于区间的加减操作转化为对于端点的操作;
时间复杂度为 $O(n)$;
用于维护区间的增减但不能维护乘除;
差分后的序列比原来的数组序列多一个数。
差分算法解题的基本思路:
设定 $b[1]=a[1]$;
对于第 2 项到第 n 项,利用差分式 $b[i]=a[i]-a[i-1]$;
对于区间端点进行加减操作;
进行差分还原(即前缀和);
注意,这里从 1 开始。如果从 0 开始,还需讨论 $i=0$ 的情况。使用 1 的话,$b[1]=a[1]-a[0]=a[1]$。
差分法的一般步骤:
假设有一个数组:
1 a = [1, 2, 3, 4, 5, 7, 2]
差分后:
1 b = [1, 1, 1, 1, 1, 2, -5]
一般应用场景是对区间 $[l,r]$ 进行 $N$ 次加减操作。例如:
从第二个元素到第五个元素每个加 $3$
从第二个元素到第四个元素每个减 $2$
从第一个元素到第三个元素每个加 $1$
对于每个 $[l,r]$ 区间的加减操作都可转化为对端点 $l$ 和 $r+1$ 的操作。例如,从第二个元素到第五个元素每个加 $3$,可转化为 $[l]$ 加 $3$ 且 $[r+1]$ 减 $3$。
原序列变成了:
1 2 1 1 1 1 1 2 -5 1 4 1 1 1 -1 -5
然后按照 $b[i]=b[i]+b[i-1]$ 复原:
去掉最后一项,跟原序列对比:
1 2 1 2 3 4 5 7 2 1 5 6 7 8 7 2
确实是都加上了 $3$。
继续操作:
从第二个元素到第四个元素每个减 $2$,可转化为 $[l]$ 减 $2$ 且 $[r+1]$ 加 $2$。
序列变成了:
1 2 1 4 1 1 1 -1 -5 1 2 1 1 3 -1 -5
然后按照 $b[i]=b[i]+b[i-1]$ 复原:
与上次复原后对比:
1 2 1 5 6 7 8 7 2 1 3 4 5 8 7 2
确实是按照操作执行了。
注意,不需要每次都复原,只需在最后一次复原即可。
最后直接做三次,最后还原:
从第二个元素到第五个元素每个加 $3$
从第二个元素到第四个元素每个减 $2$
从第一个元素到第三个元素每个加 $1$
原序列差分后:
第 2 个元素加 3
第 6 个元素减 3
第 2 个元素减 2
第 5 个元素加 2
第 1 个元素加 1
第 4 个元素减 1
差分序列变成:
复原后:
与原序列对比:
1 2 1 2 3 4 5 7 2 2 4 5 5 8 7 5
所以,差分算法是非常方便快捷的。
差分与前缀和是逆操作,常在一起出现,但是先做差分还是先做前缀和就是两种不同的算法,做不做另一种操作也决定了算法不同,所以大家要根据题目分析,具体学会使用。
大学里的树木要打药 题目描述:
教室外有 N 棵树,根据不同的位置和树种,学校要对其上不同的药。
因为树的排列成线性,且非常长,我们可以将它们看作一条直线给他们编号。
树的编号从 $0$ ~ $N-1$ 且 $N<1e6$。
对于树的药是成区间分布,比如 $3 - 5$ 号的树靠近下水道,所以他们要用驱蚊虫的药, $20 - 26$ 号的树,他们排水不好,容易涝所以要给他们用点促进根系的药。
诸如此类,每种不同的药要花不同的钱。
现在已知共有 $M$ 个这样的区间,并且给你每个区间花的钱,请问最后,这些树木花了多少药费。
输入:
输入描述:
每组输入的第一行有两个整数 $N(1 <= N<= 1000000)$和 $M(1 <= M <= 100000)$。
$N$ 代表马路的共计多少棵树,$M$代表区间的数目,$N$ 和 $M$ 之间用一个空格隔开。
接下来的 $M$ 行每行包含三个不同的整数,用一个空格隔开,表示一个区域的起始点 $L$ 和终止点 $R$ 的坐标,以及花费。
输入样例:
1 2 3 4 500 3 150 300 4 100 200 20 470 471 19
输出描述:
输出包括一行,这一行只包含一个整数,所有的花费。
输出样例:
样例:
输入样例:
1 2 3 4 5 6 7 8 9 3000 8 150 1130 2 1020 1200 3 470 2071 1 1123 211 6 12 222 2 13 23 2 1 213 4 1232 2523 6
输出样例:
运行限制:
1 1. 最大运行时间:1s 2. 最大运行内存:128M
题目解析:
利用$b[i]=a[i]-a[i-1]$ 差分式。
这里由于开始时都是 $0$,可以用,但是用完都还是 $0$,所以没有意义,所以直接跳过即可。
依次读入区间的值,然后将对于区间的操作转化为对于区间端点操作加减。 由于我们从 $1$ 开始,所以数目整体区间要右移 $1$ 位。
对于每个 $[l,r]$ 区间的加减操作都转化为对端点 $l,r+1$ 的操作。
差分还原(前缀和)。
1 2 for (int i = 1 ; i < n; i++) b[i] = a[i] - a[i - 1 ]
差分算法解决区间加减问题通用框架如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 输入n,m for (int i=1 ;i<=n;i++) { 输入a[i] }for (int i=1 ;i<=n;i++) b[i]=a[i]-a[i-1 ]while (m--) { 输入l,r,value b[l]+value b[r+1 ]-value }for (int i=1 ;i<n;i++) b[i]=b[i]+b[i-1 ]
答案解析 C++ 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> using namespace std;int b[100005 ];int main () { int n; int m; cin >> n >> m; while (m--) { int l, r, value; cin >> l >> r >> value; b[l+1 ] += value; b[r + 1 +1 ] -= value; } for (int i = 1 ; i <= n; i++) b[i] = b[i] + b[i - 1 ]; int sum = 0 ; for (int i = 1 ; i <= n; i++) sum += b[i]; cout << sum << endl; }
Python 解题代码
递推算法代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 b = [0 ] * 100005 if __name__ == '__main__' : nm = input ().split() n = int (nm[0 ]) m = int (nm[1 ]) while (m > 0 ): m -= 1 lrv = input ().split() l = int (lrv[0 ]) r = int (lrv[1 ]) value = int (lrv[2 ]) b[l+1 ] += value b[r + 1 +1 ] -= value for i in range (1 , n+1 ): b[i] = b[i - 1 ] + b[i] sum = 0 for i in range (1 ,n+1 ): sum += b[i] print (sum )
Java 解题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.util.Scanner;public class Main { static int b[]=new int [100005 ]; public static void main (String[] args) { Scanner in = new Scanner (System.in); int n; int m; n = in.nextInt(); m = in.nextInt(); while (m>0 ) { m--; int l,r,value ; l = in.nextInt(); r = in.nextInt(); value = in.nextInt(); b[l+1 ]+=value; b[r+1 +1 ]-=value; } for (int i=1 ; i<=n; i++) b[i]=b[i]+b[i-1 ]; int sum=0 ; for (int i=1 ;i<=n;i++) sum+=b[i]; System.out.println(sum); } }
前缀和 前缀和法的应用主要也是用于处理区间问题。
前缀和是指某序列的前 $n$ 项和,可以把它理解为数学上的数列的前 $n$ 项和。当对于某一数组区间进行多次询问,$[L,r]$ 的和时,如果正常处理,那么我们每次都要 $[l,r]$。查询 $N$ 次,那么时间复杂度也是 $O(nm)$ 也是平方阶的。
如果我们采用前缀和,构造出一个前缀和数组,通过对于端点的值的减法操作就能 $O(1)$ 的求出 $[l,r]$ 的和。然后 $N$ 次查询的,就将复杂度降低为 $O(n)$。
同差分一样,感觉操作变得更加复杂了,但是只用对端点值的操作确实比一整个区间相加的方法要优秀的多。听到这里大家很期待了,我们接着进行讲解。
前缀和的特点:
将对于区间的求和操作转化为对于端点值的减法的操作;
区间求和操作的时间复杂度为 $O(1)$;
数组存放时要从 $1$ 开始;
前缀和数组比原来的数组序列多一个数,第 $0$ 个元素为 $0$。
前缀和算法解题的基本思路:
利用 $\text{sum}[i]=a[i]+\text{sum}[i-1]$ 差分式;
从第 1 项到 $n$ 项,且第 $0$ 项无数据默认为 $0$;
对于区间求和的操作转化为端点值相减。
前缀和的一般解题过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 首先假设有一个数组: 1 2 3 4 5 7 2 前缀和后: 0 1 3 6 10 15 22 24 一般应用场景: 让你对区间 [l,r] 求和操作N次 如: 从第二个元素到第五个元素的和 从第二个元素到第四个元素的和 从第一个元素到第三个元素的和 .... 这里我们先演示前三个: 对于每个 [l,r] 区间的求和操作转化为区间端点的加减操作 sum[l,r] =[r]-[l-1] 从第二个元素到第五个元素的和: 转化为:[5]-[1] 那么Sum[2,5]=[5]-[1]=14 且 2+3+4+5=14 确实是相等的,就是这么神奇。 我们继续操作: 从第二个元素到第四个元素的和 转化为:[4]-[1] 那么Sum[2,4]=[4]-[1]=9 且 2+3+4=9 我们继续操作: 从第一个元素到第三个元素的和 转化为:[3]-[0] 那么Sum[1,3]=[3]-[0]=6 且 1+2+3=6 符合题意,验证结束,咱么做个题目看一看
大学里的树木要维护 题目描述:
教室外有 $N$ 棵树,根据不同的位置和树种,学校已经对其进行了多年的维护。因为树的排列成线性,且非常长,我们可以将它们看作一条直线给他们编号。
树的编号从 $1-N$ 且 $N<1e6$。由于已经维护了多年,每一个树都由学校的园艺人员进行了维护费用的统计。
每棵树的前期维护费用各不相同,但是由于未来需要要打药,所以有些树木的维护费用太高的话,就要重新种植。由于维护费用也称区间分布,所以常常需要统一个区间里的树木的维护开销。
现在园艺人员想知道,某个区间内的树木维护开销是多少。共计 $M$ 个区间需要查询。
输入描述:
每组输入的第一行有两个整数 $N(1 <= N<= 1000000)$和 $M(1 <= M <= 100000)$。
$N$ 代表马路的共计多少棵树,$M$ 代表区间的数目,$N$ 和 $M$ 之间用一个空格隔开。接下来的一行,包含 $N$ 个数,每个数之间用空格隔开。
接下来的$M$行每行包含两个不同的整数,用一个空格隔开,表示一个区域的起始点$L$和终止点$R$的坐标。
输入样例:
1 2 3 4 5 10 3 7 5 6 4 2 5 0 8 5 3 1 5 2 6 3 7
输出描述:
输出包括 $M$ 行,每一行只包含一个整数,所有的花费。
输出样例:
样例:
输入样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 30 28 172 723 580 822 718 798 941 625 450 716 540 252 16 666 115 679 274 323 875 233 99 538 881 486 610 462 319 878 930 735 6 22 7 21 3 16 7 20 9 17 0 21 13 27 7 19 10 23 2 14 21 22 15 17 6 13 16 23 21 21 11 15 5 12 9 11 8 22 10 16 3 8 15 27 5 16 4 8 0 27 4 8 7 21 20 21
输出样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 8140 6804 7918 6705 3708 10617 6576 6472 6207 7847 637 1068 4338 3902 99 1589 5040 1706 6401 2984 4484 5894 6516 3904 13913 3904 6804 332
运行限制:
1 2 1. 最大运行时间:1s 2. 最大运行内存:128M
题目解析:
利用$sum[i]=a[i]+sum[i-1]$ 前缀和式在输入时求出前缀和;
依次读入区间的值,然后将对于区间的求和操作转化为对于区间端点操作加减,对于每个 [l,r] 区间的求和操作都转化为对端点[r]-[l-1]的操作。
输出答案。
前缀和一般解题过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 输 入 N 和 M 输入 N 个值 并计算前缀和for ( int i=1 ;i<=N;i++) 输入a[i] 并计算sum[i]=sum[i-1 ]+a[i] 输入 M 个区间,计算结果while (M) M-=1 输入 L , R 计算 [r]-[l-1 ],并输出
答案解析 C++ 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> using namespace std;int a[100005 ];int sum[100005 ];int main () { int n; int m; cin >> n >> m; for (int i = 1 ; i <= n; i++) { cin >> a[i]; sum[i] = a[i] + sum[i - 1 ]; } while (m > 0 ) { m -= 1 ; int l, r; cin >> l >> r; cout << sum[r] - sum[l - 1 ] << endl; } }
这个代码有个问题,虽然是能通过的,但是他是一个输入对应一个输出的,我们之前讲过,这对大部分的测评机是没问题。
终端输出:
1 2 3 4 5 6 7 8 9 10 11 10 3 7 5 6 4 2 5 0 8 5 3 1 5 24 2 6 22 3 7 17 Process returned 0 (0x0) execution time : 1.741 s Press any key to continue.
但是如果有想要规规矩矩的处理,或者说题目要求必须全部读入后输出。我们可这样操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <bits/stdc++.h> using namespace std;int a[100005 ];int sum[100005 ]; vector<int >ss;int main () { int n ; int m; cin>>n>>m; for (int i=1 ;i<=n;i++) { cin>>a[i]; sum[i]=a[i]+sum[i-1 ]; } while (m>0 ) { m-=1 ; int l,r; cin>>l>>r; ss.push_back (sum[r]-sum[l-1 ]); } for (auto sss:ss) cout<<sss<<endl; }
终端输出:
1 2 3 4 5 6 7 8 9 10 11 10 3 7 5 6 4 2 5 0 8 5 3 1 5 2 6 3 7 24 22 17 Process returned 0 (0x0) execution time : 6.235 s Press any key to continue.
都可以,大家看自己需求和心情选择即可。
Python 解题代码
普通代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 a = [0 ] * 100005 sum1 = [0 ] * 100005 if __name__ == '__main__' : nm = input ().split() n = int (nm[0 ]) m = int (nm[1 ]) a = input ().split() a = list (map (int , a)) for i in range (1 , n + 1 ): sum1[i] = sum1[i - 1 ] sum1[i] += a[i - 1 ] while m > 0 : m -= 1 lrv = input ().split() l = int (lrv[0 ]) r = int (lrv[1 ]) print (sum1[r] - sum1[l - 1 ])
特殊代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 a = [0 ] * 100005 sum1 = [0 ] * 100005 if __name__ == '__main__' : nm = input ().split() n = int (nm[0 ]) m = int (nm[1 ]) a = input ().split() a = list (map (int , a)) for i in range (1 , n + 1 ): sum1[i] = sum1[i - 1 ] sum1[i] += a[i - 1 ] ans = [] while m > 0 : m -= 1 lrv = input ().split() l = int (lrv[0 ]) r = int (lrv[1 ]) ans.append(sum1[r] - sum1[l - 1 ]) for i in ans: print (i)
Java 解题代码
普通算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import java.util.Scanner;public class Main { static int a[]=new int [100005 ]; static int sum[]=new int [100005 ]; public static void main (String[] args) { Scanner in = new Scanner (System.in); int n; int m; n = in.nextInt(); m = in.nextInt(); for (int i=1 ;i<=n;i++) { a[i]= in.nextInt(); sum[i]=a[i]+sum[i-1 ]; } while (m>0 ) { m--; int l,r; l = in.nextInt(); r = in.nextInt(); System.out.println((sum[r]-sum[l-1 ])); } } }
特殊代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import java.util.Scanner;import java.util.Vector;public class Main { static int a[]=new int [100005 ]; static int sum[]=new int [100005 ]; static Vector ans=new Vector <Integer>(); public static void main (String[] args) { Scanner in = new Scanner (System.in); int n; int m; n = in.nextInt(); m = in.nextInt(); for (int i=1 ;i<=n;i++) { a[i]= in.nextInt(); sum[i]=a[i]+sum[i-1 ]; } while (m>0 ) { m--; int l,r; l = in.nextInt(); r = in.nextInt(); ans.addElement(sum[r]-sum[l-1 ]); } for (Object ab:ans){ System.out.println(ab); } } }
总结 我们这节课讲了差分和前缀和的知识点,并且也讲了怎样使用差分,怎样使前缀和,也讲了差分和前缀和最常见的两种情况。
差分和前缀和是很多思维题的解题技巧,必须要掌握熟练才能拿到简单题目的全部分数。