序列差分
复习前缀和
之前我们学习过前缀和。
我们对一个数组 构造出它的前缀和数组 ,其中
那么我们也可以在给定了 b 数组内的值的情况下去构造 a 数组。我们令即可。我们把使用 b 数组构造 a 数组的操作称为序列差分。实际上,这两种操作互逆。
差分 场景
接下来我们来看一下这样的操作有什么用处。
现在我们在一个序列 上需要进行这种操作若干次:
给定三个数 l,r,x,将 全部加上一个数 x。
操作完毕之后我们需要把新序列输出。
使用暴力算法,我们可以这么做:
for (int i = 1; i <= m; i++) {
cin >> l >> r >> x;
for (int j = l; j <= r; j++) {
a[j] += x;
}
}
算法的时间复杂度是 ,其中 m 是操作总次数。因为我们需要对区间内的每个数逐个修改。
但是现在我们可以发现这样一个结论,在完成操作之后:
- 的值相对于操作之前增加了 x。
- 的值相对于操作之前减少了 x。
- 对于 而言, 是不变的。
至于其他的部分,我们就不用再关心了。而根据这个结论,我们把 这个区间内 个数的变化变成了 和 这两个值的变化。
由此我们其实就可以构造出 的差分数组 ,并且随着每一次修改操作,我们都只需要修改数组里面的两个值。
而其实我们还有一种理解方式:
- 的值加上 x。
- 的值再减去 x。
这样你就可以和前缀和结合起来。无论你用哪种方式去理解,最后的代码实现都是一样的。
我们令 ,那么就可以把代码改成:
for (int i = 1; i <= m; i++) {
cin >> l >> r >> x;
d[l] += x;
d[r + 1] -= x;
}
我们把所有操作完成之后,需要将 数组还原成 数组,而
于是我们只需要一个循环就把整个序列还原出来了。
for (int i = 1; i <= n; i++) {
a[i] = a[i - 1] + d[i];
}
实现差分序列
#include <iostream>
using namespace std;
const int maxn = 110;
int a[maxn], d[maxn];
int main() {
int n;
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> a[i];
}
for(int i = 1; i <= n; ++i){
d[i] = a[i]-a[i-1];
}
int q;
cin >> q;
while(q--){
int l , r, v ;
cin >> l >> r >> v;
d[l] += v;
d[r+1] -= v;
}
for(int i = 1; i <= n; ++i){
a[i] = a[i-1] + d[i];
cout << a[i] << " ";
}
return 0;
}
二维差分
一维差分是非常简单的。那么我们现在把这个问题扩展一下:
给定五个数 表示将矩阵 里面第 行 列到第 行 列的数都加上 。
一切结束之后,你需要把整个矩阵每个元素的值输出。
我们首先模仿一维前缀和去预处理一个“二维前缀和”,我们定义一个数组 , 表示矩阵 里面第 1 行 1 列到第 行 列的数的和。有:
我们将 移动到式子的一边,有:
我们定义从矩阵 sum 推出矩阵 a 的操作为对矩阵 sum 进行 二维差分。
我们可以参照下面这张图来理解第一个式子:
我们首先将 加起来,然后发现图上橙色的部分(也就是 ) 被算了两次,所以需要减去 ,最后得到 sum_{i,j}。
于是我们对矩阵 也可以进行 二维差分,我们构造出一个差分矩阵,其中有
那么对于将矩阵 里面第 行 列到第 行 列的数都加上 这样一个操作,在 数组上我们就可以这样呈现:
- 以第 行第 列为左上角的子矩阵内所有元素加上 x。
- 以第 行第 列为左上角的子矩阵内所有元素减去 x。
- 以第 行第 列为左上角的子矩阵内所有元素减去 x。
- 以第 行第 列为左上角的子矩阵内所有元素加上 x。
for (int i = 1; i <= k; i++) {
cin >> x1 >> y1 >> x2 >> y2 >> x;
d[x1][y1] += x;
d[x2 + 1][y1] -= x;
d[x1][y2 + 1] -= x;
d[x2 + 1][y2 + 1] += x;
}
二维差分 完整代码
#include <iostream>
using namespace std;
const int maxn = 110;
int a[maxn][maxn], d[maxn][maxn];
int main() {
int n , m , k;
cin >> n >> m >> k;
for(int i = 1;i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> a[i][j];
d[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1];
}
}
int x1,y1,x2,y2,x;
for(int i = 1; i <= k; i++){
cin >> x1 >>y1 >> x2 >> y2 >> x;
d[x1][y1] += x;
d[x2 + 1][y1] -= x;
d[x1][y2 + 1] -= x;
d[x2+1][y2 + 1] += x;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + d[i][j];
cout << a[i][j] << ' ';
}
cout << endl;
}
return 0;
}
exm
差分模型
构造类问题是一类非常宽泛的问题,可能会涉及到你现在已经学到的任何东西。 这里我们所说的差分模型,便是构造类问题的其中一种。
在对差分模型进行构造的时候,你需要记住一条铁律:差分 是一个在序列上(矩阵中)进行的操作。
所以当你尝试构造差分模型去解决一个问题的时候,原序列(矩阵)是什么,便是你需要注意的第一件事情。而原序列的构造一般和题目中的提问直接相关。接下来我们来看一道例题。
春春是一名道路工程师,负责铺设一条长度为 n 的道路。
铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n 块首尾相连的区域,一开始,第 i 块区域下陷的深度为 di。
春春每天可以选择一段连续区间 [L,R],填充这段区间中的每块区域,让其下陷深度减少 1。在选择区间时,需要保证,区间内的每块区域在填充前下陷深度均不为 0。
春春希望你能帮他设计一种方案,可以在最短的时间内将整段道路的下陷深度都变为 0。
输入格式
输入文件包含两行,第一行包含一个整数 n,表示道路的长度。第二行包含 n 个整数,相邻两数间用一个空格隔开,第 i 个整数为 di。
输出格式
输出文件仅包含一个整数,即最少需要多少天才能完成任务。
输入样例
6 4 3 2 5 3 5
输出样例
9
样例解释:
一种可行的最佳方案是,依次选择:
一种可行的方式是:定义 为把道路的前 处坑填平花的时间,这个和题目的询问直接相关。所以现在我们要解决的问题是: 的结果是什么。
的含义是,在计算出填平前 个坑的天数之后,加入了第 个坑给答案 带来的变化。
那么我们来具体思考一下:首先你肯定需要把第 块区域给填平。然后你考虑将第 块区域加入到计算中。因为我们要最少的天数,并且每次填充的连续区间大小是不限制的,所以,在我们选择一个区间 进行填充时,是可以将这个区间改为 的。
简单点说,那就是我们在填第 块区域的时候,会同时填充第 块区域。这样会产生下面两种情况:
- 如果第 块区域比第 块区域的坑浅,那么第 块区域被填平的时候,第 块区域也一定已经被填平了。
- 如果第 块区域比第 块区域的坑深,那么第 块区域被填平的时候,第 块区域被填的部分深度和第 块区域的深度一定是一样的。
由此 的结果便可以分成两种情况:
- 。
code
#include <iostream>
using namespace std;
int main() {
freopen("road.in", "r", stdin);
freopen("road.out", "w", stdout);
int n;
scanf("%d", &n);
int ans = 0, x = 0, h;
for (int i = 1; i <= n; i++) {
scanf("%d", &h);
if(h > x){
ans = ans + h -x;
}else{
ans += 0;
}
x = h;
}
cout << ans << endl;
return 0;
}