差分是个好东西,当遇到一个差分题而你没有差分思想,此题将难如登天。
点名批评积木大赛(
一般涉及到某些区间的操作我们可能就会想到 线段树 差分,差分在这些题目中有着优秀的性质,可以将原本需要O(n)时间的区间操作降为O(1)。
纯粹的差分题并不多,一般以一个优化步骤的形式存在于各种各样的题目中......
差分模板题
题目描述
输入一个长度为 n n n 的整数序列。
接下来输入 m m m 个操作,每个操作包含三个整数 l , r , c l, r, c l,r,c,表示将序列中 [ l , r ] [l, r] [l,r] 之间的每个数加上 c c c。
请你输出进行完所有操作后的序列。
输入格式
- 第一行包含两个整数 n n n 和 m m m。
- 第二行包含 n n n 个整数,表示整数序列。
- 接下来 m m m 行,每行包含三个整数 l , r , c l, r, c l,r,c,表示一个操作。
输出格式
- 共一行,包含 n n n 个整数,表示最终序列。
数据范围
- 1 ≤ n , m ≤ 100 , 000 1 \le n, m \le 100,000 1≤n,m≤100,000
- 1 ≤ l ≤ r ≤ n 1 \le l \le r \le n 1≤l≤r≤n
- − 1000 ≤ c ≤ 1000 -1000 \le c \le 1000 −1000≤c≤1000
- − 1000 ≤ 整数序列中元素的值 ≤ 1000 -1000 \le \text{整数序列中元素的值} \le 1000 −1000≤整数序列中元素的值≤1000
输入样例
text
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例
text
3 4 5 3 4 2
思路:一维差分
1. 核心思想
差分可以看作是前缀和的逆运算。
- 假设原数组为 a [ 1 ] , a [ 2 ] , ... , a [ n ] a[1], a[2], \dots, a[n] a[1],a[2],...,a[n]。
- 我们构造一个差分数组 b [ 1 ] , b [ 2 ] , ... , b [ n ] b[1], b[2], \dots, b[n] b[1],b[2],...,b[n],使得 a [ i ] = ∑ j = 1 i b [ j ] a[i] = \sum_{j=1}^{i} b[j] a[i]=∑j=1ib[j](即 a a a 是 b b b 的前缀和数组)。
2. 核心操作
要在原数组 a a a 的 [ l , r ] [l, r] [l,r] 区间内每个数都加上 c c c,在差分数组 b b b 上仅需两次 O ( 1 ) O(1) O(1) 的操作:
- b [ l ] + = c b[l] += c b[l]+=c:这会导致从 a [ l ] a[l] a[l] 开始及之后的所有前缀和元素都增加 c c c。
- b [ r + 1 ] − = c b[r + 1] -= c b[r+1]−=c:这会让从 a [ r + 1 ] a[r+1] a[r+1] 开始及之后的所有元素减去 c c c,从而抵消掉第一个操作对区间 [ r + 1 , n ] [r+1, n] [r+1,n] 的影响。
通过这种方式,区间修改的效率从 O ( n ) O(n) O(n) 提升到了 O ( 1 ) O(1) O(1)。最后对 b b b 数组求一遍前缀和,即可在 O ( n ) O(n) O(n) 时间内得到修改后的 a a a 数组。
3. 实现技巧
在处理初始化时,可以将原数组的输入也看作是 n n n 次区间为 [ i , i ] [i, i] [i,i]、增加值为 a [ i ] a[i] a[i] 的插入操作,这样可以统一代码逻辑。
代码
具体实现中可以省掉一个数组的空间。
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+10;
int n, m, a[N];
int main()
{
scanf("%d%d", &n, &m);
int x=0,y;
for(int i=1; i<=n; i++){
scanf("%d", &y);
a[i]=y-x; // 存差分数组
x=y;
}
// 修改差分数组
while(m--){
int l,r,c;
scanf("%d%d%d", &l, &r, &c);
a[l]+=c;
a[r+1]-=c;
}
x=0;
// 输出当前差分数组的前缀和
for(int i=1; i<=n; i++){
printf("%d ",a[i]+x);
x=a[i]+x;
}
return 0;
}
一阶差分------积木大赛
该题跟noip2018铺设道路也是同一道题,跟usaco13 Poker Hands S 也是同一道题。差分变化千千万,一阶差分都一样~
题目背景
NOIP2013 提高组 D2T1
题目描述
春春幼儿园举办了一年一度的"积木大赛"。今年比赛的内容是搭建一座宽度为 n n n 的大厦,大厦可以看成由 n n n 块宽度为 1 1 1 的积木组成,第 i i i 块积木的最终高度需要是 h i h_i hi。
在搭建开始之前,没有任何积木(可以看成 n n n 块高度为 0 0 0 的积木)。接下来每次操作,小朋友们可以选择一段连续区间 [ l , r ] [l, r] [l,r],然后将第 L L L 块到第 R R R 块之间(含第 L L L 块和第 R R R 块)所有积木的高度分别增加 1 1 1。
小 M 是个聪明的小朋友,她很快想出了建造大厦的最佳策略,使得建造所需的操作次数最少。但她不是一个勤于动手的孩子,所以想请你帮忙实现这个策略,并求出最少的操作次数。
输入格式
包含两行,第一行包含一个整数 n n n,表示大厦的宽度。
第二行包含 n n n 个整数,第 i i i 个整数为 h i h_i hi。
输出格式
建造所需的最少操作数。
输入输出样例 #1
输入 #1
text
5
2 3 4 1 2
输出 #1
text
5
说明/提示
样例解释
其中一种可行的最佳方案,依次选择: [ 1 , 5 ] [1,5] [1,5], [ 1 , 3 ] [1,3] [1,3], [ 2 , 3 ] [2,3] [2,3], [ 3 , 3 ] [3,3] [3,3], [ 5 , 5 ] [5,5] [5,5]。
思路:差分的应用
1. 问题转化
题目要求将全 0 0 0 序列变为目标序列 h h h,每次操作可以将区间 [ l , r ] [l, r] [l,r] 加 1 1 1。这等价于求:最少进行多少次区间加法操作,能凑出目标差分数组。
2. 分析
设目标序列为 h h h,其差分数组为 d d d,其中 d i = h i − h i − 1 d_i = h_i - h_{i-1} di=hi−hi−1(约定 h 0 = 0 h_0 = 0 h0=0)。
- 每次区间修改 [ l , r ] [l, r] [l,r] 会使 d l d_l dl 增加 1 1 1,使 d r + 1 d_{r+1} dr+1 减少 1 1 1。
- 我们的目标是让初始全 0 0 0 的差分数组最终变为 d d d。
- 每一个正的差分值 d i > 0 d_i > 0 di>0 都意味着必须有 d i d_i di 次操作以 i i i 作为左端点开始增加高度。
- 负的差分值则可以通过作为操作的右端点来"消耗"掉。(该点需要严谨证明正负差分值的匹配性:负的差分值可以被消耗完)
3. 结论
最少操作次数等于所有正差分值之和:
Ans = ∑ i = 1 n max ( 0 , h i − h i − 1 ) \text{Ans} = \sum_{i=1}^{n} \max(0, h_i - h_{i-1}) Ans=i=1∑nmax(0,hi−hi−1)
也可以理解为:如果 h i > h i − 1 h_i > h_{i-1} hi>hi−1,则第 i i i 块积木比前一块多出的高度必须由新的操作产生,增加次数为 h i − h i − 1 h_i - h_{i-1} hi−hi−1;如果 h i ≤ h i − 1 h_i \le h_{i-1} hi≤hi−1,则第 i i i 块的高度可以在搭建第 i − 1 i-1 i−1 块时顺便完成。
补充证明:正负差分值的匹配性
按逆向思维,将区间 [ l , r ] [l, r] [l,r] 高度减 1 1 1 等价于差分操作 d l ← d l − 1 d_l \gets d_l - 1 dl←dl−1 与 d r + 1 ← d r + 1 + 1 d_{r+1} \gets d_{r+1} + 1 dr+1←dr+1+1。证明的关键在于:对于每一个需要减量的 d i > 0 d_i > 0 di>0,其后方是否总有足够的负数空间来接收增量。
观察差分数组的前缀和性质: ∑ k = 1 i d k = h i \sum_{k=1}^{i} d_k = h_i ∑k=1idk=hi。由于目标高度始终满足 h i ≥ 0 h_i \ge 0 hi≥0,这说明在差分序列 d d d 的任何位置,其左侧正数之和一定大于或等于负数绝对值之和。
从全局来看,若补位 h n + 1 = 0 h_{n+1} = 0 hn+1=0,则整个差分数组的总和 ∑ d = 0 \sum d = 0 ∑d=0。这保证了每一个正差分值 d i d_i di 产生的减法需求,都能通过与后方的负差分值匹配来完成。因此,我们只需要关注所有正差分值的累加 ∑ max ( 0 , d i ) \sum \max(0, d_i) ∑max(0,di),即可得到完成构建所需的最少操作次数。
代码
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int n,ans;
int main()
{
scanf("%d", &n);
int x,y;
scanf("%d", &x);
ans+=x;
for(int i=1; i<=n; i++){
scanf("%d", &y);
if(y>x) ans+=(y-x);
x=y;
}
printf("%d",ans);
return 0;
}
小练习
(并非小练习(实则是拓展
简单练习------[USACO05FEB] Feed Accounting S
其实本题非常简单,数据范围很小,暴力直接能过。差分优化也很明显。
cpp
#include<iostream>
#include<cstdio>
using namespace std;
const int N=2010;
int c,f1,f2,f,d,cnt;
int t[N];
int main(){
scanf("%d%d%d%d",&c,&f1,&f2,&d);
f=f1-f2;
while(c--){
int l,r;
scanf("%d%d",&l,&r);
if(r>d) r=d;
for(int i=l; i<=r; i++){
t[i]++;
}
}
for(int i=d; i>=1; i--){
cnt+=t[i];
if(cnt==f){
printf("%d",i);
break;
}
}
return 0;
}
不过其实从暴力的过程很明显可以看到能用差分数组优化的地方,也就是遍历每一个小段的区间,给其中的每个数++。一阶差分之后就可以只操作两端。
cpp
#include<iostream>
#include<cstdio>
using namespace std;
const int N=2010;
int c,f1,f2,f,d,cnt;
int t[N];
int main(){
scanf("%d%d%d%d",&c,&f1,&f2,&d);
f=f1-f2;
while(c--){
int l,r;
scanf("%d%d",&l,&r);
if(r>d) r=d;
t[l]++;
t[r+1]--;
}
int x=0;
for(int i=1; i<=d; i++){
t[i]=t[i]+x;
x=t[i];
}
for(int i=d; i>=1; i--){
cnt+=t[i];
if(cnt==f){
printf("%d",i);
break;
}
}
return 0;
}
对一阶差分更本质的理解------[USACO21DEC] Convoluted Intervals S
这题对数据范围的要求更严格了, n n n 的范围很大但是值域 m m m 较小,所以单纯的差分( O ( n 2 ) O(n^2) O(n2))会t,需要用桶优化。这里对桶的操作,分离了经典差分操作的各个小区间的前后端点。
在看这道题之前,我们回看一下经典差分操作的核心步骤:
cpp
while(m--){
int l,r,c;
scanf("%d%d%d", &l, &r, &c);
d[l]+=c;
d[r+1]-=c;
}
可以看到就是在差分数组上对 l r 进行单点修改。而对于每一组 l 和 r,在实际意义上它们是一一配对的,但是在操作的时候是分离 的,互不影响的。也就是说我们根本不需要关心每个 l 对应的 r 是哪个,只需要知道这 m 个小区间里的 l 有哪些、 r 有哪些。
所以这个题我们就可以愉快地用桶了。
ta[i]=x 存 i 作为左端点出现了 x 次,tb[i]=x 存 i 作为右端点出现了 x 次,d[i] 作为差分数组,存的是当 k=i 时的最终答案的差分。
那么在这道题中的 l 就是所有的 i+j,其出现次数为 ta[i]*ta[j] (0<i,j<=m) (乘法原理),r 同理。
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int M = 5010;
int n,m;
ll ta[M], tb[M], d[2*M];
ll ans;
int main(){
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++){
int l,r;
scanf("%d%d", &l, &r);
ta[l]++, tb[r]++;
}
for(int i=0; i<=m; i++){
for(int j=0; j<=m; j++){
d[i+j]+=ta[i]*ta[j];
d[i+j+1]-=tb[i]*tb[j];
}
}
ll x=0;
for(int i=0; i<=2*m; i++){
printf("%lld\n",x+d[i]);
x=d[i]+x;
}
return 0;
}
输出不同的方案数------[Poetize6] IncDec Sequence
这题主要是多问了一个方案数,要推结论。
题目描述
给定一个长度为 n n n 的数列 a 1 , a 2 , ⋯ , a n {a_1,a_2,\cdots,a_n} a1,a2,⋯,an,每次可以选择一个区间 [ l , r ] [l,r] [l,r],使这个区间内的数都加 1 1 1 或者都减 1 1 1。
请问至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列有多少种。
第一行一个正整数 n n n。 接下来 n n n 行,每行一个整数,第 i + 1 i+1 i+1 行的整数表示 a i a_i ai。
第一行输出最少操作次数。 第二行输出最终能得到多少种结果。
输入
4
1
1
2
2
输出
1
2
思路
这道题的目标是让数列所有数相等。在差分数组 s s s 的视角下,如果 a 1 = a 2 = ⋯ = a n a_1 = a_2 = \cdots = a_n a1=a2=⋯=an,那么 s 2 = s 3 = ⋯ = s n = 0 s_2 = s_3 = \cdots = s_n = 0 s2=s3=⋯=sn=0,而 s 1 s_1 s1(即 a 1 a_1 a1)可以是任何值。
我们的目标变成了:用最少的操作,把 s [ 2 ... n ] s[2 \dots n] s[2...n] 全变成 0 0 0。
在差分数组上,区间操作 [ l , r ] [l, r] [l,r] 加 1 1 1 相当于: s [ l ] + 1 s[l] + 1 s[l]+1 且 s [ r + 1 ] − 1 s[r+1] - 1 s[r+1]−1。
为了让操作最快,我们有三种有用选择:
- 最优选 :选一个 s [ i ] > 0 s[i] > 0 s[i]>0 和一个 s [ j ] < 0 s[j] < 0 s[j]<0(其中 i , j ∈ [ 2 , n ] i, j \in [2, n] i,j∈[2,n])。一次操作可以同时让两个数向 0 0 0 靠拢。
- 次优选1 :选 s [ i ] s[i] s[i] 与 s [ 1 ] s[1] s[1] 配合。
- 次优选2 :选 s [ i ] s[i] s[i] 与 s [ n + 1 ] s[n+1] s[n+1] 配合( s [ n + 1 ] s[n+1] s[n+1] 无实际意义,相当于只改一个数)。
示例 A:1 1 2 2 3 3
a = [1, 1, 2, 2, 3, 3]
s = [1, 0, 1, 0, 1, 0]
-1 -1 +2
+1 -1 -1 +1
+2 -1 -1
可以看到两个-1的位置是固定的,而两个+1的位置是自由的,有三种方案。
示例 B:2 2 1 1 2 2 1 1
a = [2, 2, 1, 1, 2, 2, 1, 1]
s = [2, 0, -1, 0, 1, 0, -1, 0]
-1 +1 -1 +1
+1 -1 +1 -1
两个+1和一个-1的位置是固定的,一个-1是自由的,两种方案。
差不多就能总结出规律了。
结论:
- 最少次数 :尽可能多地执行第 1 种操作,剩下的再执行第 2 或第 3 种。次数就是 max ( p o s , n e g ) \max(pos, neg) max(pos,neg)。
- 结果种数 :最后剩下的 ∣ p o s − n e g ∣ |pos - neg| ∣pos−neg∣ 个单位操作,可以分给 s [ 1 ] s[1] s[1](改变最终的数值),也可以分给 s [ n + 1 ] s[n+1] s[n+1](不改变最终数值)。分法有 ∣ p o s − n e g ∣ + 1 |pos - neg| + 1 ∣pos−neg∣+1 种。
代码
cpp
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e5+10;
typedef long long ll;
int n;
ll a[N], s[N];
ll pos, neg, ans, cnt;
int main()
{
scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%lld", &a[i]);
s[i]=a[i]-a[i-1];
if(i==1) continue;
if(s[i]>0) pos+=s[i];
else neg-=s[i];
}
ans=max(pos, neg);
cnt=abs(pos-neg)+1;
printf("%lld\n%lld",ans,cnt);
return 0;
}
异或区间的差分------[USACO07MAR] Face The Right Way G
异或差分的关键在于用一个bool值b存当前位置累计的状态变化量,和一个状态差分数组 f[i] 存第 i 个及其之后的状态变化量。
N N N 头牛排成一列。每头牛要么向前要么向后。为了让所有牛都面向前方,农夫每次可以将 K K K 头连续的牛转向( 1 ≤ K ≤ N 1 \le K \le N 1≤K≤N),求最小的操作次数 M M M 和相应的最小 K K K。
第一行一个正整数 N N N。下面 N N N 行,每行一个字符 F 或 B,表示一头奶牛的初始朝向。(F 为朝前,B 为朝后)
请在一行输出两个数字 K K K 和 M M M,用空格分开。
输入
7
B
B
F
B
F
B
B
输出
3 3
思路
这道题的核心是通过枚举 K K K + 贪心策略 + 异或差分优化 ,将复杂度从 O ( N 3 ) O(N^3) O(N3) 降到 O ( N 2 ) O(N^2) O(N2)。
1. 贪心策略:确定"必翻"位置
对于固定的 K K K,我们从左到右扫描。如果第 i i i 头牛当前是 B,那么它必须作为翻转区间的起点。因为左边的牛已经修好了,你以后再也不可能在不破坏左边的情况下修好它。
2. 核心巧思:异或差分 (XOR Difference)
这是本题最精妙的地方。我们不需要真的去翻转区间里的 K K K 头牛,而是维护一个"翻转信号灯" b。
- 状态传递 (
b) :变量b代表当前位置受到的翻转次数(奇数次翻转 b = 1 b=1 b=1,偶数次 b = 0 b=0 b=0)。 - 实际状态判定 :牛的真实朝向 =
初始状态 ^ b。 - O ( 1 ) O(1) O(1) 模拟区间翻转:
- 开启信号 :当发现需要翻转时,令
b ^= 1。这一步相当于告诉后面的牛:"你们都被笼罩在翻转效果里了"。 - 埋下地雷 (
f[i+K]) :在区间结束后的第一个位置标记f[i+K] = 1。 - 自动撤销 :每移动到一个新位置 i i i,执行
b ^= f[i]。如果正好踩到了之前埋下的"地雷",b会再次异或 1 变回原样,翻转信号自动消失。
总结步骤:
对于每一个可能的区间长度 K ∈ [ 1 , n ] K \in [1, n] K∈[1,n]:
- 用
b ^= f[i]更新当前翻转状态。 - 用
a[i] ^ b判断是否需要翻转。 - 若需要,在 i + K i+K i+K 处打标记,更新
b和操作次数cnt。 - 若最后 N − K + 1 N-K+1 N−K+1 位之后还有
B,说明该 K K K 无效。
最终取最优解,记录操作次数 cnt 最小的 K K K。
复杂度分析
- 空间 : O ( N ) O(N) O(N),仅需存储初始状态和差分标记。
- 时间 : O ( N 2 ) O(N^2) O(N2)。枚举 K K K 需要 N N N 次,每次差分扫描需要 N N N 次。如果不用差分优化的话是 O ( N 3 ) O(N^3) O(N3)。
代码
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 5010;
int n, ans = 1e9, ans_k, a[N];
bool f[N];
int main(){
scanf("%d", &n);
for(int i=1; i<=n; i++){
getchar();
char c=getchar();
if(c=='B') a[i]=0;
else a[i]=1;
}
for(int k=1; k<=n; k++){
bool b=0, fl=0; int cnt=0;
memset(f, 0, sizeof f);
for(int i=1; i<=n; i++){
b^=f[i]; // 更新当前位置的翻转状态
if((a[i]^b)==0){ // 如果是0 就要翻转
if(i+k-1>n){ // i+k-1内还有B 则该k下无解
fl=1;
break;
}
b^=1; // 翻转需要改当前的状态b
f[i+k]^=1; // 标记k后再改一次翻转状态
cnt++; // 操作数++
}
}
if(fl) continue;
if(ans>cnt){
ans=cnt;
ans_k=k;
}
}
printf("%d %d",ans_k,ans);
return 0;
}
下面来到二阶差分
二阶差分------平衡细菌
P10133 [USACO24JAN] Balancing Bacteria B
题目描述
Farmer John 有 N N N( 1 ≤ N ≤ 2 ⋅ 10 5 1\le N\le 2\cdot 10^5 1≤N≤2⋅105)块草地排成一行,其中草地 i i i 的细菌水平与健康草的细菌水平相差 a i a_i ai( − 10 15 ≤ a i ≤ 10 15 −10^{15}\le a_i\le 10^{15} −1015≤ai≤1015)。例如,如果 a i = − 3 a_i=−3 ai=−3,则草地 i i i 的细菌水平比正常水平低 3 3 3,需要额外添加恰好 3 3 3 个单位的细菌才能将其提高到被认为是健康的程度。
Farmer John 想要确保每一块草地都被修复至健康的细菌水平。他有两种品牌的农药,一种可以添加细菌,另一种可以去除细菌。当 Farmer John 喷洒任一类型的农药时,他站在草地 N N N 并选择功率等级 L L L( 1 ≤ L ≤ N 1\le L\le N 1≤L≤N)。
喷雾器效果随着距离增加逐渐减弱。如果 Farmer John 选择添加细菌的农药,则 L L L 单位的细菌将被添加至草地 N N N, L − 1 L−1 L−1 单位添加至草地 N − 1 N−1 N−1,以此类推。草地 1 ... N − L 1\ldots N−L 1...N−L 不会得到任何细菌。类似地,如果 Farmer John 选择去除细菌的农药,则 L L L 单位被从草地 N N N 去除, L − 1 L−1 L−1 被从草地 N − 1 N−1 N−1 去除,以此类推。草地 1 ... N − L 1\ldots N−L 1...N−L 将不受影响。
求 Farmer John 使用喷雾器的最少次数,使得每块草地都具有健康草的推荐细菌值。输入保证答案不超过 10 9 10^9 109。
注意这个问题涉及到的整数可能需要使用 64 位整数型(例如,C/C++ 中的 "long long")。
输入格式
输入的第一行包含 N N N。
第二行包含 N N N 个整数 a 1 ... a N a_1\ldots a_N a1...aN,为每块草地的初始细菌水平。
输出格式
输出一个整数,为使每块草地都具有健康草的推荐的细菌值所需使用喷雾器的最少次数。
输入输出样例 #1
输入 #1
text
2
-1 3
输出 #1
text
6
输入输出样例 #2
输入 #2
text
5
1 3 -2 -7 5
输出 #2
text
26
说明/提示
样例解释 1
使用去除细菌的农药(功率 L = 1 L=1 L=1)五次,然后使用添加细菌的农药(功率 L = 2 L=2 L=2)一次。
思路:二阶差分
喷雾操作对区间 [ i , n ] [i, n] [i,n] 的影响是一个首项为 1 1 1、公差为 1 1 1 的等差数列 (即 1 , 2 , 3 , ... 1, 2, 3, \dots 1,2,3,...)。这种操作在不同阶数的差分中具有如下性质:
- 原数组 a a a:受到线性增长的影响。
- 一阶差分 d d d ( a i − a i − 1 a_i - a_{i-1} ai−ai−1):将线性影响转化为区间加法(常数增长)。
- 二阶差分 d d dd dd ( d i − d i − 1 d_i - d_{i-1} di−di−1):将区间加法进一步转化为单点修改。
要使原数组 a a a 归零,等价于将其二阶差分数组 d d dd dd 的每一项都变为 0 0 0。由于在位置 i i i 启动喷雾只会影响 d d i dd_i ddi 而不影响其左侧元素,因此每个位置 i i i 必须且只需进行 ∣ d d i ∣ |dd_i| ∣ddi∣ 次操作。总操作次数即为二阶差分数组绝对值的总和:
Ans = ∑ i = 1 n ∣ d d i ∣ \text{Ans} = \sum_{i=1}^{n} |dd_i| Ans=i=1∑n∣ddi∣
代码
也可以通过倒序遍历省掉一个数组的空间,但是可读性会降低。这里还是保留易于理解的版本。
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e5+10;
typedef long long LL;
int n;
LL a[N], d[N], dd[N];
LL cnt;
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++){
scanf("%lld", &a[i]);
}
for(int i=1; i<=n; i++){
d[i]=a[i]-a[i-1];
}
for(int i=1; i<=n; i++){
dd[i]=d[i]-d[i-1];
cnt+=abs(dd[i]);
}
printf("%lld",cnt);
return 0;
}
小总结
可以发现对于一阶差分的题而言,小区间对于结果的影响是常数,比如说对区间[l,r] 范围内的结果都加c,而二阶差分则呈等差数列(一次函数y=x)变化。那么,区间内的影响呈 y=x^k 变化的,就可以用 k+1 阶差分。
二维一阶差分------地毯
题目描述
在 n × n n\times n n×n 的格子上有 m m m 个地毯。
给出这些地毯的信息,问每个点被多少个地毯覆盖。
输入格式
第一行,两个正整数 n , m n,m n,m。意义如题所述。
接下来 m m m 行,每行两个坐标 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 和 ( x 2 , y 2 ) (x_2,y_2) (x2,y2),代表一块地毯,左上角是 ( x 1 , y 1 ) (x_1,y_1) (x1,y1),右下角是 ( x 2 , y 2 ) (x_2,y_2) (x2,y2)。
输出格式
为了减少输出量,设 F i , j F_{i,j} Fi,j 表示 ( i , j ) (i,j) (i,j) 这个格子被多少个地毯覆盖,你只需要输出 ∑ i = 1 n ∑ j = 1 n ( i + j ) ⊕ F i , j \sum_{i=1}^n\sum_{j=1}^n (i+j)\oplus F_{i,j} ∑i=1n∑j=1n(i+j)⊕Fi,j 的值。注意这个值可能会超过 2 31 2^{31} 231。
输入 #1
5 3
2 2 3 3
3 3 5 5
1 2 1 4
输出 #1
146
说明/提示
对于 50 % 50\% 50% 的数据,有 n , m ≤ 5000 n,m\le 5000 n,m≤5000。
对于 100 % 100\% 100% 的数据,有 n ≤ 5000 n\le 5000 n≤5000, m ≤ 2 × 10 5 m\le 2\times 10^5 m≤2×105。
思路
本质上是二维差分。当然差分也伴随着前缀和。二维的问题会稍微复杂一些。
这里我们以方格描述题目中的点,和坐标系的标法不一样,但是更直观一些。
另外,在算法竞赛和一般计算机中表述的坐标系其实都是屏幕坐标系,原点在左上方,纵轴为x,横轴为y。
理解差分数组 c[i][j] : c[i][j]++ 表示从这个点开始,往右下方的所有区域覆盖层数 + 1 +1 +1。

假设一个铺设地毯,则要修改以下这四个位置的差分。
c[x1][y1]++;
c[x2+1][y1]--;
c[x1][y2+1]--;
c[x2+1][y2+1]++;

最后得到完整的差分数组 c[][] 。而表示每个位置放了多少地毯的原数组 s[][] ,也就是 c[][] 的二维前缀和:

根据图可以得到以下这个递推关系:
s[i][j] = c[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1]
代码
当然,s数组可以不开,全都用原先的差分数组,公式不变,因为按照遍历顺序可以直接覆盖,类似于dp的状态转移。
cpp
#include<iostream>
#include<cstdio>
using namespace std;
const int N=5010;
typedef long long ll;
int n,m;
int c[N][N],s[N][N];
ll ans;
int main(){
scanf("%d%d",&n,&m);
while(m--){
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
c[x2+1][y2+1]++;
c[x2+1][y1]--;
c[x1][y2+1]--;
c[x1][y1]++;
}
for(int i=1; i<=n; i++){
for(int j=1; j<=n; j++){
s[i][j]=c[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1];
ans+=((ll)(i+j)^(s[i][j]));
}
}
printf("%lld", ans);
return 0;
}