差分

1,226 阅读3分钟

序列差分

复习前缀和

之前我们学习过前缀和。

我们对一个数组 {a1,a2,...,an}\{ a_1,a_2,...,a_n\} 构造出它的前缀和数组 {b1,b2,...,bn}\{b_1,b_2,...,b_n\},其中

bi=j=1iajb_i=\sum_{j=1}^ia_j

那么我们也可以在给定了 b 数组内的值的情况下去构造 a 数组。我们令{ai=bibi1}\{a_i = b_i-b_i-1\}即可。我们把使用 b 数组构造 a 数组的操作称为序列差分。实际上,这两种操作互逆。

差分 场景

接下来我们来看一下这样的操作有什么用处。

现在我们在一个序列  {a1,a2,...,an}\{ a_1,a_2,...,a_n\} {a1,a2,...,an}\{ a_1,a_2,...,a_n\} 上需要进行这种操作若干次:

给定三个数 l,r,x,将  [al,...,ar][ a_l,...,a_r] 全部加上一个数 x。

操作完毕之后我们需要把新序列输出。

使用暴力算法,我们可以这么做:

for (int i = 1; i <= m; i++) {
    cin >> l >> r >> x;
    for (int j = l; j <= r; j++) {
        a[j] += x;
    }
}

算法的时间复杂度是  O(mn) O(mn),其中 m 是操作总次数。因为我们需要对区间内的每个数逐个修改。

image.png 但是现在我们可以发现这样一个结论,在完成操作之后:

  1. alal1a_l-a_{l-1}的值相对于操作之前增加了 x。
  2. ar+1ara_{r+1}-a_r的值相对于操作之前减少了 x。
  3. 对于 li<rl≤i<r 而言,ai+1aia_{i+1}−a_i 是不变的。

至于其他的部分,我们就不用再关心了。而根据这个结论,我们把 [alar][al∼ar] 这个区间内 rl+1r−l+1 个数的变化变成了 alal1a_l−a_{l−1} 和 ar+1ara_{r+1}−a_r 这两个值的变化。

由此我们其实就可以构造出 aa 的差分数组 dd,并且随着每一次修改操作,我们都只需要修改 d  d 数组里面的两个值。

而其实我们还有一种理解方式:

  • alana_l∼a_n 的值加上 x。
  • ar+1ana_{r+1}∼a_n的值再减去 x。

这样你就可以和前缀和结合起来。无论你用哪种方式去理解,最后的代码实现都是一样的。

我们令 di=aiai1d_i=a_i−a_{i−1},那么就可以把代码改成:

for (int i = 1; i <= m; i++) {
    cin >> l >> r >> x;
    d[l] += x;
    d[r + 1] -= x;
}

我们把所有操作完成之后,需要将 dd 数组还原成 aa 数组,而

ai=j=1idja_i=\sum_{j=1}^id_j

于是我们只需要一个循环就把整个序列还原出来了。

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;  
}

二维差分

一维差分是非常简单的。那么我们现在把这个问题扩展一下:

给定五个数 x1,y1,x2,y2,xx_1,y_1,x_2,y_2,x,表示将矩阵 aa 里面第 x1x_1 行 y1y_1列到第 x2x_2 行 y2 y_2 列的数都加上 xx

image.png

一切结束之后,你需要把整个矩阵每个元素的值输出。

我们首先模仿一维前缀和去预处理一个“二维前缀和”,我们定义一个数组 sumsumsumi,jsum_{i,j} 表示矩阵 aa 里面第 1 行 1 列到第 ii 行 jj 列的数的和。有:

sumi,j=sumi1,j+sumi,j1sumi1,j1+ai,jsum_{i,j}=sum_{i−1,j}+sum_{i,j−1}−sum_{i−1,j−1}+a_{i,j}。

image.png

我们将 ai,jai,j移动到式子的一边,有:

ai,j=sumi,jsumi1,jsumi,j1+sumi1,j1a_{i,j}=sum_{i,j}−sum_{i−1,j}−sum_{i,j−1}+sum_{i−1,j−1}。

我们定义从矩阵 sum 推出矩阵 a 的操作为对矩阵 sum 进行 二维差分。

我们可以参照下面这张图来理解第一个式子:

image.png

我们首先将 sumi1,j,sumi,j1,ai,j sum_{i−1,j},sum_{i,j−1},a_{i,j} 加起来,然后发现图上橙色的部分(也就是 sumi1,j1sum_{i−1,j−1}) 被算了两次,所以需要减去 sumi1,j1sum_{i−1,j−1},最后得到 sum_{i,j}。

于是我们对矩阵 aa 也可以进行 二维差分,我们构造出一个差分矩阵 d d,其中有

di,j=ai,jai1,jai,j1+ai1,j1d_{i,j}=a_{i,j}−a_{i−1,j}−a_{i,j−1}+a_{i−1,j−1}

那么对于将矩阵 aa 里面第 x1x_1 行 y1y_1 列到第 x2x_2 行 y2y_2 列的数都加上 xx 这样一个操作,在 dd 数组上我们就可以这样呈现:

  1. 以第 x1x_1 行第 y1y_1 列为左上角的子矩阵内所有元素加上 x。
  2. 以第 x1x_1 行第 y2+1 y_2+1 列为左上角的子矩阵内所有元素减去 x。
  3. 以第 x2+1x_2+1 行第 y1 y_1 列为左上角的子矩阵内所有元素减去 x。
  4. 以第 x2+1x_2+1 行第 y2+1y_2+1 列为左上角的子矩阵内所有元素加上 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

样例解释:

一种可行的最佳方案是,依次选择: [1,6][1,6][1,2][1,1][4,6][4,4][4,4][6,6][6,6][1,6]、[1,6]、[1,2]、[1,1]、[4,6]、[4,4]、[4,4]、[6,6]、[6,6]。

一种可行的方式是:定义 fif_i 为把道路的前 ii 处坑填平花的时间,这个和题目的询问直接相关。所以现在我们要解决的问题是:fifi1f_i−f_{i−1} 的结果是什么。

fifi1f_i−f_{i−1}的含义是,在计算出填平前 i1i−1 个坑的天数之后,加入了第 ii 个坑给答案 带来的变化。

那么我们来具体思考一下:首先你肯定需要把第 i1 i−1 块区域给填平。然后你考虑将第 ii 块区域加入到计算中。因为我们要最少的天数,并且每次填充的连续区间大小是不限制的,所以,在我们选择一个区间 [j,i1](1j<i)[j,i−1](1≤j<i) 进行填充时,是可以将这个区间改为 [j,i](1ji)[j,i](1≤j≤i) 的。

简单点说,那就是我们在填第 i1i−1 块区域的时候,会同时填充第 ii 块区域。这样会产生下面两种情况:

  • 如果第 ii 块区域比第 i1i−1 块区域的坑浅,那么第 i1i−1 块区域被填平的时候,第 ii 块区域也一定已经被填平了。
  • 如果第 ii 块区域比第 i1 i−1 块区域的坑深,那么第 i1i−1 块区域被填平的时候,第 ii 块区域被填的部分深度和第 i1i−1 块区域的深度一定是一样的。

由此 fifi1 f_i−f_{i−1} 的结果便可以分成两种情况:

  • didi10d_i≤d_{i−1}:0。
  • di>di1didi1d_i>d_{i−1}:d_i−d_{i−1}

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;
}

练习