【基础算法】差分算法详解

发布于:2025-06-08 ⋅ 阅读:(18) ⋅ 点赞:(0)

算法简介

差分前缀和是一对互逆的运算,前缀和算法通过预处理原数组,将原数组的区间和保存在前缀和数组中从而达到快速查询某段区间的和的目的。而差分则是将一个数组看作前缀和数组,计算出原数组的过程

差分的作用快速解决将某一个区间所有元素统一加上/减去一个数的操作。此操作时间复杂度 O ( 1 ) O(1) O(1)


1. 【模板】一维差分

【题目链接】

【模板】差分_牛客题霸_牛客网

请添加图片描述


(1) 差分数组的原理

请添加图片描述
由这个定义可以推导出,如果原数组在 [L, R] 区间上的数都统一加上一个数 c,那么在对应的差分数组中,仅需让 f[L] += c 以及 f[R + 1] += c 即可。

请添加图片描述

证明

  1. L 位置左边的每一个元素与前一个元素的差值不变,差分数组不变;
  2. L 位置的元素与前一个元素的差值增加了 c,因此差分数组的 L 位置应该加上 c,即 f[L] += c
  3. L 位置与 R 位置之间的每一个元素与前一个元素的差值不变,因此差分数组不需要修改;
  4. R 位置右边的 R + 1 位置对应的元素与前一个位置对应元素的差值减少了 c,因此差分数组对应的 R + 1 位置上应该减去 c,即 f[R + 1] -= c
  5. R + 1 位置之后的每一个元素与前一个元素的差值不变,差分数组不变。

(2) 差分数组的构建

① 利用定义构建差分数组

a[] 为原数组,f[] 为差分数组。因为差分数组的每一个值为原数组对应位置的值减去前一个位置的值,所以我们可以通过定义直接构建出差分数组 f[]

// 利用定义求差分数组
for(int i = 1; i <= n; i++)
{
    cin >> a[i];
    f[i] = a[i] - a[i - 1];
}

② 利用性质构建差分数组

在实际中我们更多地采用这种方式来构建差分数组,因为这个时候我们就不需要原数组 a[] 了,仅需一个 f[] 差分数组即可。我们先创建一个数组 int f[N],里面的值都默认为 0。当我们读入一个数 x 放入到该数组的一个位置 f[i] ,**相当于在原数组中的第 i 个位置上加上 x。**那么由差分数组的性质,我们可以直接将 f[i] += x 以及 f[i + 1] -= x 即可。

// 利用性质求差分数组
for(int i = 1; i <= n; i++)
{
    int x; cin >> x;
    f[i] += x;
    f[i + 1] -= x;
}

(3) 代码实现

  • 利用定义构建差分数组
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;
int n, m;
LL a[N];  // 原数组
LL f[N];  // 差分数组 

int main()
{
    cin >> n >> m;
    
    // 利用定义求差分数组
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        f[i] = a[i] - a[i - 1];
    }

    // 处理 m 次修改操作
    while(m--)
    {
        LL l, r, k;
        cin >> l >> r >> k;
        f[l] += k;
        f[r + 1] -= k;
    }

    // 还原出修改后的数组
    for(int i = 1; i <= n; i++)
    {
        a[i] = a[i - 1] + f[i];
        cout << a[i] << " ";
    }

    return 0;
}
  • 利用性质构建差分数组
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;
int n, m;
LL f[N];  // 差分数组 

int main()
{
    cin >> n >> m;
    
    // 利用性质求差分数组
    for(int i = 1; i <= n; i++)
    {
        LL x; cin >> x;
        f[i] += x;
        f[i + 1] -= x;
    }

    // 处理 m 次修改操作
    while(m--)
    {
        LL l, r, k;
        cin >> l >> r >> k;
        f[l] += k;
        f[r + 1] -= k;
    }

    // 还原出修改后的数组
    for(int i = 1; i <= n; i++)
    {
        f[i] = f[i - 1] + f[i];
        cout << f[i] << " ";
    }

    return 0;
}

(4) 注意事项

在使用差分数组的时候,只有当所有的操作全部执行完毕后,才能还原出原数组。因此,如果是每操作若干次就查询一次操作后的结果,然后再操作再查询,这样的情况就不能使用差分数组。


2. 海底高铁

【题目链接】

P3406 海底高铁 - 洛谷

【题目描述】

该铁路经过 N N N 个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好,于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。第 i i i 段铁路连接了城市 i i i 和城市 i + 1 ( 1 ≤ i < N ) i+1(1\leq i<N) i+1(1i<N)。如果搭乘的比较远,需要购买多张车票。第 i i i 段铁路购买纸质单程票需要 A i A_i Ai 博艾元。

虽然一些事情没有协调好,各段铁路公司也为了方便乘客,推出了 IC 卡。对于第 i i i 段铁路,需要花 C i C_i Ci 博艾元的工本费购买一张 IC 卡,然后乘坐这段铁路一次就只要扣 B i ( B i < A i ) B_i(B_i<A_i) Bi(Bi<Ai) 元。IC 卡可以提前购买,有钱就可以从网上买得到,而不需要亲自去对应的城市购买。工本费不能退,也不能购买车票。每张卡都可以充值任意数额。对于第 i i i 段铁路的 IC 卡,无法乘坐别的铁路的车。

Uim 现在需要出差,要去 M M M 个城市,从城市 P 1 P_1 P1 出发分别按照 P 1 , P 2 , P 3 , ⋯   , P M P_1,P_2,P_3,\cdots,P_M P1,P2,P3,,PM 的顺序访问各个城市,可能会多次访问一个城市,且相邻访问的城市位置不一定相邻,而且不会是同一个城市。

现在他希望知道,出差结束后,至少会花掉多少的钱,包括购买纸质车票、买卡和充值的总费用。

【输入格式】

第一行两个整数, N , M N,M N,M

接下来一行, M M M 个数字,表示 P i P_i Pi

接下来 N − 1 N-1 N1 行,表示第 i i i 段铁路的 A i , B i , C i A_i,B_i,C_i Ai,Bi,Ci

【输出格式】

一个整数,表示最少花费

【示例一】

输入

9 10
3 1 4 1 5 9 2 6 5 3
200 100 50
300 299 100
500 200 500
345 234 123
100 50 100
600 100 1
450 400 80
2 1 10

输出

6394

【说明/提示】

2 2 2 3 3 3 以及 8 8 8 9 9 9 买票,其余买卡。

对于 30 % 30\% 30% 数据 M = 2 M=2 M=2

对于另外 30 % 30\% 30% 数据 N ≤ 1000 , M ≤ 1000 N\leq1000,M\leq1000 N1000M1000

对于 100 % 100\% 100% 的数据 M , N ≤ 10 5 , A i , B i , C i ≤ 10 5 M,N\leq 10^5,A_i,B_i,C_i\le10^5 M,N105Ai,Bi,Ci105


(1) 解题思路

  1. 对于从第 i 段铁路到第 i + 1 段铁路的最小花费,我们需要直到经过此段铁路的总次数,记作 f[i]
  2. 如果这段铁路不买卡,那么一次花费 a 元,总共花费 a * f[i] 元。
  3. 如果这段铁路买卡,那么总共的花费为 c + b * f[i]
  4. 总共的最小花费为 min(a * f[i], c + b * f[i])i = 1,2,3……)。

关键在于如何求得每段铁路经过了多少次,即如何求得 f[i]?

此时就需要使用差分数组了。每次经过一个区间上的铁路相当于对数组的某一个区间上的每一个值加一,假设起始的铁路的位置为 x,终点为 y ( x < y ),那么将 f[i] 看作差分数组,仅需让 f[x]++ 以及 f[y]-- 即可。

注意不是 f[y + 1]--,因为我们规定的是从第 i 段铁路到第 i + 1 段铁路记作 f[i],终点为 y 即最后一段铁路为从 y - 1y


(2) 代码实现

#include<iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;

int n, m;
LL f[N];  // 差分数组

int main()
{
    cin >> n >> m;
    int x; cin >> x;

    // 求经过每段铁路的次数(构建差分数组)
    for(int i = 2; i <= m; i++)
    {
        int y; cin >> y;
        if(x > y) f[y]++, f[x]--;
        else f[x]++, f[y]--;
        x = y;
    }

    // 利用差分数组还原出原数组
    for(int i = 1; i <= n; i++) f[i] += f[i - 1];

    LL ans = 0;
    for(int i = 1; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        ans += min(f[i] * a, f[i] * b  + c);
    }

    cout << ans;
    
    return 0;
}

3. 【模板】二维差分

【题目链接】

【模板】二维差分_牛客题霸_牛客网

请添加图片描述

一维差分是在一维数组上进行差分,而二维差分则是在一个矩阵(二维数组)上进行差分。相应的我们就需要求出对应的差分矩阵。

二维差分的作用是:快速将矩阵中的某一个子矩阵中的所有元素加上/减去一个值


(1) 差分矩阵的性质

在差分矩阵中进行前缀和运算,可以还原出原数组。抓牢这一点,我们可以有以下分析:

假设我们需要将原矩阵 a 中,以 (x1, y1) 为左上角,(x2, y2) 为右下角的子矩阵中的每一个元素都加上 k,那么原矩阵对应的差分矩阵应该进行什么样的操作呢?

请添加图片描述

如图所示,在差分矩阵中:

  • 我们将 (x1, y1) 的位置也加上 k,那么求前缀和后,紫色区域能够还原出原矩阵,但是绿、黄、粉色区域会被多加上 k
  • 为了抵消这个多的 k,我们需要在绿、黄色区域的左上角减去一个 k,这样求前缀和时绿、黄色区域便能够还原出原矩阵,但是粉色区域会被多减去一个 k
  • 因此,我们还需要在粉色区域的左上角减去一个 k,这样一来,我们对整个差分数组求前缀和便可以还原出原矩阵了。

所以我们可以得出结论,当以 (x1, y1) 为左上角,(x2, y2) 为右下角的子矩阵中的每一个元素都加上 k 时,对应的差分矩阵应进行的操作为:

f[x1][y1] += k;
f[x1][y2 + 1] -= k;
f[x2 + 1][y1] -= k;
f[x2 + 1][y2 + 1] += k;

(2) 差分矩阵的构建

当我们需要对一个矩阵 a[][] 构建出对应的差分矩阵时,我们首先创建一个差分矩阵 f[][],其每一个位置的值默认为 0。当我们读入原矩阵一个对应位置 a[i][j] = k 时,相当于在差分矩阵中以 (i, j) 为左上角,(i, j) 为右下角的子矩阵中的每一个元素都加上 k 。只需:

f[i][j] += k;
f[i + 1][j] -= k;
f[i][j + 1] -= k;
f[i + 1][j + 1] += k;

(3) 代码实现

#include<iostream>

using namespace std;

typedef long long LL;

const int N = 1010;
LL f[N][N];  // 差分矩阵
int m, n, q;

// 子矩阵统一加上 k 时差分矩阵需要修改的操作
void change(int x1, int y1, int x2, int y2, LL k)
{
    f[x1][y1] += k;
    f[x2 + 1][y1] -= k;
    f[x1][y2 + 1] -= k;
    f[x2 + 1][y2 + 1] += k;
}

int main()
{
    cin >> m >> n >> q;
    for(int i = 1; i <= m; i++)
    {
        for(int j = 1; j <= n; j++)
        {   
            LL x; cin >> x;
            change(i, j, i, j, x);  // 边读入边构建差分矩阵
        }
    }

    while(q--)
    {
        int x1, y1, x2, y2, k;
        cin >> x1 >> y1 >> x2 >> y2 >> k;
        change(x1, y1, x2, y2, k);
    }

    for(int i = 1; i <= m; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            // 做前缀和运算求出原始矩阵
            f[i][j] += f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1];
            cout << f[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}

4. 地毯

【题目链接】

P3397 地毯 - 洛谷

【题目描述】

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)

【输出格式】

输出 n n n 行,每行 n n n 个正整数。

i i i 行第 j j j 列的正整数表示 ( i , j ) (i,j) (i,j) 这个格子被多少个地毯覆盖。

【示例一】

输入

5 3
2 2 3 3
3 3 5 5
1 2 1 4

输出

0 1 1 1 0
0 1 1 0 0
0 1 2 1 1
0 0 1 1 1
0 0 1 1 1

【说明/提示】

样例解释

覆盖第一个地毯后:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 1 1 0 0 0 0 0 0
0 0 0 1 1 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

覆盖第一、二个地毯后:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 1 1 0 0 0 0 0 0
0 0 0 1 1 1 2 2 2 1 1 1 1 1 1
0 0 0 0 0 0 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 1 1 1 1 1 1 1 1 1

覆盖所有地毯后:

0 0 0 1 1 1 1 1 1 1 1 1 0 0 0
0 0 0 1 1 1 1 1 1 0 0 0 0 0 0
0 0 0 1 1 1 2 2 2 1 1 1 1 1 1
0 0 0 0 0 0 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 1 1 1 1 1 1 1 1 1

【数据范围】

对于 20 % 20\% 20% 的数据,有 n ≤ 50 n\le 50 n50 m ≤ 100 m\le 100 m100

对于 100 % 100\% 100% 的数据,有 n , m ≤ 1000 n,m\le 1000 n,m1000


(1) 解题思路

这道题可以说和模板题几乎一模一样,只不过每个子矩阵加上的数 k `变成了 1 然后套了个情景,思路和模板一模一样,利用差分矩阵求解即可。


(2) 代码实现

#include<iostream>

using namespace std;

const int N = 1010;
int f[N][N];  // 差分矩阵
int n, m;

// 一张地毯覆盖后,差分矩阵需要修改的操作
void cover(int x1, int y1, int x2, int y2)
{
    f[x1][y1]++;
    f[x2 + 1][y1]--;
    f[x1][y2 + 1]--;
    f[x2 + 1][y2 + 1]++;
}

int main()
{
    cin >> n >> m;
    while(m--)
    {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cover(x1, y1, x2, y2);
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            // 求前缀和还原出原矩阵
            f[i][j] += f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1];
            cout << f[i][j] << " ";
        }
        cout << endl;
    }
    
    return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到