算法基础详解(三)前缀和与差分算法

欢迎来到我的频道[【点击跳转专栏】]

作者说:我想说 基础 不等于 简单 ;算法能力不是一蹴而就的,而是来自日积月累的积累和练习!积小流终成江海,诸君 加油!!

文章目录

  • 1.前缀和
    • [1.1 一维前缀和](#1.1 一维前缀和)
    • [1.2 最⼤⼦段和](#1.2 最⼤⼦段和)
    • [1.3 二维前缀和](#1.3 二维前缀和)
    • [1.4 激光炸弹](#1.4 激光炸弹)
  • [2. 差分](#2. 差分)
    • [2.1 一维差分](#2.1 一维差分)
    • [2.2 海底高铁](#2.2 海底高铁)
    • [2.3 二维差分(超级难感觉~)](#2.3 二维差分(超级难感觉~))
    • [2.4 地毯](#2.4 地毯)

1.前缀和

前缀和与差分的核心思想是预处理,可以在暴力枚举的过程中,快速给出查询的结果,从而优化时间复杂度。

是经典的用空间替换时间的做法。

1.1 一维前缀和

https://ac.nowcoder.com/acm/problem/226282

前缀和模板题,直接套用「公式」创建前缀和数组,然后利用前缀和数组的「性质」处理 q q q 次查询。

  1. 创建前缀和数组: f [ i ] = f [ i − 1 ] + a [ i ] f[i] = f[i - 1] + a[i] f[i]=f[i−1]+a[i]
  2. 查询 [l, r] 区间和: f[r] − f[l − 1]
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=1e5+10;
int l,r,n,m;
LL a[N],t[N];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
     cin>>a[i];
     t[i]=t[i-1]+a[i];
    }
    while(m--)
    {
        cin>>l>>r;
        cout<<t[r]-t[l-1]<<endl;
    }
}

1.2 最⼤⼦段和

https://www.luogu.com.cn/problem/P1115

考虑以 (i) 位置的元素 (a[i])「为结尾」的最大子段和:

  • 在求「区间和」时,相当于是用 (f[i]) 减去 (i) 位置前面的某一个 (f[x]);(f[x]为最小的值!)
  • 如果想要「最大子段和」,也就是「最大区间和」,那么用 f[i] 减掉一个「前驱最小值」即可。

因此,我们可以创建 a数组的「前缀和」数组,然后在遍历前缀和数组的过程中,一边「更新前驱最小值」,一边「更新当前位置为结尾的最大子段和」。

通俗的说 就是:用f[i]减去[1,i-1]1中所有前缀和的最小值即可!

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int a[N],t[N];
int n;

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        t[i]=t[i-1]+a[i];
    }
    int ret = -0x3f3f3f3f;
    int prevmin=0;//前面的最小值

    for(int i=1;i<=n;i++)
    {
        ret=max(ret,t[i]-prevmin);
        prevmin=min(prevmin,t[i]);
    }
    cout<<ret;
}

1.3 二维前缀和

https://ac.nowcoder.com/acm/problem/226333


二维前缀和模板题,直接套用「公式」创建前缀和矩阵,然后利用前缀和矩阵的「性质」处理 (q) 次询问。

  1. 创建前缀和矩阵:f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j]
  2. 查询以 (x1 , y1 ) 为左上⻆ , (x2 , y2 ) 为右下⻆的⼦矩阵的和

⚠️:做这种题目,没必要死记公式 画一画就可以了

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e3+10;
LL a[N][N],t[N][N];
int m,n, q;
int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            cin>>a[i][j];
            t[i][j]=t[i-1][j]+t[i][j-1]-t[i-1][j-1]+a[i][j];
        }
    }
    LL ret;
    while(q--)
    {
        int x1,x2,y1,y2;
        cin>>x1>>y1>>x2>>y2;
        ret=t[x2][y2]-t[x1-1][y2]-t[x2][y1-1]+t[x1-1][y1-1];
        cout<<ret<<endl;
    }
}

1.4 激光炸弹

https://www.luogu.com.cn/problem/P2280

  1. 在坐标系中,枚举出所有变长为m的正方形,然后找出正方形中目标价值最大的即可!
    如何枚举边⻓为 R 的所有正⽅形:

仅需枚举右下⻆(x2,y2)即可 ,那么结合边⻓x2-x1+1=m 就可算出左上⻆的坐标(x2-m+1,y2-m+1)

然后利用二维前缀和求出最大值即可!

⚠️:

  1. 题目中某一个位置会「重复」出现,因此 (a[i][j] += w);
  2. 半径 (R) 可能「大于等于5001」,此时炸弹可以摧毁所有目标,也就是整个矩阵的目标价值之和。
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5010;
int a[N][N],t[N][N];
int n,m;
int main()
{
    cin>>n>>m;
    while(n--)
    {
        int x,y,v;
        cin>>x>>y>>v;
        x++,y++;
        a[x][y]+=v;
        
    }
    n=5001;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            t[i][j]=t[i-1][j]+t[i][j-1]-t[i-1][j-1]+a[i][j];
        }
    }
   int ret=0;
    //如果m>=5001 相当于全部炸毁!
    m=min(n,m);
    //枚举
    for(int i=m;i<=n;i++)
    {
        for(int j=m;j<=n;j++)
        {
            int x=i-m+1,y=j-m+1;
            ret=max(ret,t[i][j]-t[i][y-1]-t[x-1][j]+t[x-1][y-1]);
        }
    }
    cout<<ret;
}

2. 差分

前缀和与差分的核心思想是预处理,可以在暴力枚举的过程中,快速给出查询的结果,从而优化时间复杂度。是经典的用空间替换时间的做法。

学完差分之后会发现,前缀和与差分是一对互逆的运算。

2.1 一维差分

https://ac.nowcoder.com/acm/problem/226303

差分有什么作用?

快速解决"将某一个区间所有元素统一加上或减去一个数"的操作!(O(1)
差分模板题,先「创建」差分数组,然后根据差分数组的「性质」处理 q 次区间修改,最后「还原」出来原始的数组。

  1. 创建差分数组,根据定义:(f[i] = a[i] - a[i - 1])f[i]表示当前元素与前一个元素的差值!

  1. 利用差分数组解决m次修改操作(难点

性质:原数组[L,R]区间加上 k 的这个操作,相当于差分数组中,f[L]+=k,f[R+1]-=k!


  1. 性质的证明!

  1. 数组的还原(重点!

    可以用这段代码:

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

  2. 初始状态(差分数组) : 在执行循环之前,数组 f 存储的是差分值

    • 假设原始数组为 A A A,差分数组为 D D D。
    • 此时 f[i] 存储的是 D i = A i − A i − 1 D_i = A_i - A_{i-1} Di=Ai−Ai−1。
  3. 还原过程(前缀和) : 循环中的核心语句是 f[i] = f[i - 1] + f[i];

    • 这一步利用了差分的逆运算------前缀和
    • 数学原理: A i = ∑ k = 1 i D k = A i − 1 + D i A_i = \sum_{k=1}^{i} D_k = A_{i-1} + D_i Ai=∑k=1iDk=Ai−1+Di。
    • 代码中,f[i-1] 已经是还原后的 A i − 1 A_{i-1} Ai−1,而等号右边的 f[i] 还是差分值 D i D_i Di。两者相加,就得到了原始值 A i A_i Ai,并覆盖回 f[i]
  4. 输出cout << f[i] << " "; 输出的是还原后的原始数组元素。 📝 示例: 假设原始数组是 [1, 2, 3, 4, 5]

  5. 差分数组 f 初始化为:[1, 1, 1, 1, 1] (即 2 − 1 , 3 − 2 , ... 2-1, 3-2, \dots 2−1,3−2,...,首项为1)。

  6. 执行代码

    • i=1: f[1] = f[0] + f[1] = 0 + 1 = 1
    • i=2: f[2] = f[1] + f[2] = 1 + 1 = 2
    • i=3: f[3] = f[2] + f[3] = 2 + 1 = 3
    • ...
  7. 结果f 变回了 [1, 2, 3, 4, 5]


补充:其实也可以直接根据差分数组的性质进行创建差分数组(更常用!因为这样的好处就是直接不需要创建原始数组了!
f[i] + = a[i], f[i + 1] − = a[i]

相当于每次读入一个a[i] 此时f[i]就会加一个a[i] 而f[i+1]此时就减去一个a[i]

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL f[N];
int n,m,l,r,k;

int main()
{
    cin>>n>>m;
    //创建差分数组
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        f[i]+=x;
        f[i+1]-=x;
    }
    while(m--)
    {
        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]<<" ";
    }
}

2.2 海底高铁

https://www.luogu.com.cn/problem/P3406


其实这道题的难点 我个人认为最大的就是读懂题目 ! 其次才是解法!

  1. 先考虑如何让花费最小,想要求最小花费,需要知道每一段高铁被「乘坐了多少次」,记作 f [ i ] f[i] f[i],那么最小花费就是「买票的花费」与「买卡的花费」两者之间的最小值:
  • 买票花费: a [ i ] × f [ i ] a[i] \times f[i] a[i]×f[i];
  • 买卡花费,乘车花费 + 工本费: b [ i ] × f [ i ] + c [ i ] b[i] \times f[i] + c[i] b[i]×f[i]+c[i];
  • 那么最小花费就是: m i n c o s t = m i n ( a [ i ] × f [ i ] , b [ i ] × f [ i ] + c [ i ] ) mincost = min(a[i] \times f[i], b[i] \times f[i] + c[i]) mincost=min(a[i]×f[i],b[i]×f[i]+c[i])

2 . 接下来考虑如何求出每一段高铁被「乘坐了多少次」。根据访问城市的序列 p 1 , p 2 , p 3 , ... , p m p_1, p_2, p_3, \dots, p_m p1,p2,p3,...,pm

可知,对于任意一次访问 p i ∼ p i + 1 p_i \sim p_{i+1} pi∼pi+1,我们会乘坐 [ p i , p i + 1 − 1 ] [p_i, p_{i+1} - 1] [pi,pi+1−1] 之间所有的高铁,比如:
p i = 3 , p i + 1 = 6 p_i = 3, p_{i+1} = 6 pi=3,pi+1=6,那么 [ 3 , 5 ] [3, 5] [3,5] 之间所有的高铁都会被乘坐一次,相当于每个数都加上 1,「注意 6位置不会乘坐到」。那么我们就可以利用「差分数组」:

  • 创建一个全为 0 的差分数组 f f f;
  • 遍历访问序列,对于每一次访问: f [ p i ] + + , f [ p i + 1 ] − − f[p_i] ++, f[p_{i+1}] -- f[pi]++,f[pi+1]−−;
  • 然后对差分数组做一次前缀和,就得到每个高铁乘坐的次数

注意城市访问的序列有可能 p i > p i + 1 p_i > p_{i+1} pi>pi+1,此时应该「交换」一下顺序。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL f[N];
LL n,m,a,b,c;
int main()
{
    cin>>n>>m;
    int x;
    cin>>x;
    for(int i=2;i<=m;i++)
    {
        int y;
        cin>>y;
        if(x<y)
        {
            f[x]++;
            f[y]--;
        }
        else
        {
            f[y]++;
            f[x]--;
        }
        x=y;
    }
    //还原差分数组
    for(int i=1;i<n;i++)
    {
        f[i]+=f[i-1];
    }

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

2.3 二维差分(超级难感觉~)

https://ac.nowcoder.com/acm/problem/226337




这道题目的要求就是需要我们快速处理 二维数组中,让某一个子矩阵加上或者减去一个元素 的操作!

可以类⽐「⼀维差分数组」的性质,推导出「⼆维差分矩阵」的性质:

  • 在差分数组中某个位置标记:表⽰后续元素统⼀被修改;
  • 在差分数组中求前缀和:能够还原出原始数组。
    即:s[i][j] = s[i-1][j] + s[i][j-1] -s[i-1][j-1] + f[i][j]
    假设我们需要将原始矩阵 a a a 中,以 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 为左上角, ( x 2 , y 2 ) (x_2, y_2) (x2,y2) 为右下角的子矩阵的每个元素都加上 k k k:

具体操作如下:

  1. 标记增量

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

为什么是这 4 个点? 想象一下,当你对二维数组做前缀和

(即 s[i][j] = s[i-1][j] + s[i][j-1] -s[i-1][j-1] + f[i][j])[这块不懂的去看二维前缀和部分!]时,每一个点 f[i][j] 的值会向右下方无限传递。

  • 操作 1 f[x1][y1] += k
    相当于在 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 点了一个灯。做前缀和后,这个 k k k 会传递给整个右下角区域 (从 x 1 x_1 x1 到 n n n,从 y 1 y_1 y1 到 n n n 的所有格子都会 + k +k +k)。
    • 结果:影响范围太大了,不仅覆盖了目标矩形,还覆盖了右边和下边。
  • 操作 2 f[x1][y2+1] -= k 和 操作 3 diff[x2+1][y1] -= k
    为了把影响范围限制住,我们需要把多出来的部分减掉。
    • 在 y 2 + 1 y_2+1 y2+1 处减 k k k,是为了抵消掉目标矩形右侧 多出来的 + k +k +k。
    • 在 x 2 + 1 x_2+1 x2+1 处减 k k k,是为了抵消掉目标矩形下方 多出来的 + k +k +k。
  • 操作 4 diff[x2+1][y2+1] += k
    注意!当你执行操作 2 和 3 时,右下角那块区域( x > x 2 x > x_2 x>x2 且 y > y 2 y > y_2 y>y2)被减了两次 (因为它既在右侧,又在下方)。
    根据容斥原理,多减了一次,所以要加回来一次。

  1. 图解还原过程

假设我们有一个 5 × 5 5 \times 5 5×5 的矩阵,目标是将中间红色区域( 2 , 2 2,2 2,2 到 4 , 4 4,4 4,4)加 k k k。

第一步:标记差分数组

复制代码
  1  2  3  4  5
1 .  .  .  .  .
2 . +k  .  . -k
3 .  .  .  .  .
4 .  .  .  .  .
5 . -k  .  . +k

第二步:做前缀和(累加影响)

  1. 遇到 +k (左上角)
    前缀和开始累加,从这个点开始,右下方的所有格子都受到了 +k 的影响。
    • 此时整个右下大区域都是 + k +k +k。
  2. 遇到 -k (左下角)
    前缀和遇到了 -k,抵消了之前的 +k
    • 此时,目标区域右侧的格子变成了 k − k = 0 k - k = 0 k−k=0。影响被切断。
  3. 遇到 -k (右上角)
    同理,前缀和遇到了 -k,抵消了之前的 +k
    • 此时,目标区域下方的格子变成了 k − k = 0 k - k = 0 k−k=0。影响被切断。
  4. 遇到 +k (右下角)
    这个位置原本因为左上角的 +k 变成了 k k k,又因为上方和左边的两个 -k 变成了 k − k − k = − k k - k - k = -k k−k−k=−k。
    最后加上这个位置的 +k,变成了 − k + k = 0 -k + k = 0 −k+k=0。
    • 完美归零!

总结

差分矩阵做前缀和之所以能还原,是因为:

  • +k 开启了一个向右下无限延伸的"增益场"。
  • 两个 -k 分别切断了向右和向下的延伸。
  • 最后一个 +k 修复了被重复切断的角落。

最终,只有目标子矩阵区域保留了 +k,其余区域通过正负抵消归零。


数组的还原:

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

  1. 读取旧值f[i][j] 在这一刻还仅仅是差分数组在该点的值(增量)。
  2. 读取邻居f[i-1][j]f[i][j-1] 已经被更新过了,它们代表了上方和左方已经计算好的前缀和。
  3. 计算并覆盖:算出新的前缀和,覆盖掉原来的差分值。

⚠️:这个规则不要背,需要的时候画一下推导就行!

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1010;
LL f[N][N];
int main()
{
    int n,m,q;
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            int k;
            cin>>k;
            //把图上性质的(x1,y1) ~ (x2,y2) 换成(i,j)~ (i,j)
            f[i][j]+=k;
            f[i+1][j]-=k;
            f[i][j+1]-=k;
            f[i+1][j+1]+=k;
        }
    }
    
    // 处理 q 次修改操作
    while(q--)
    {
        int x1,x2,y1,y2,k;
        cin>>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;
    }
    
    //还原数组 求前缀和!
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+f[i][j];
            cout<<f[i][j]<<" ";
        }
        cout<<endl;
    }
    
}

2.4 地毯

https://www.luogu.com.cn/problem/P3397



练手题 如果你二维差分弄明白了 这道题有手就行!

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N][N];
int n,m;
int main()
{
    cin>>n>>m;
   while(m--)
   {
        int x1,x2,y1,y2;
        cin>>x1>>y1>>x2>>y2;
        f[x1][y1]++;
        f[x1][y2+1]--;
        f[x2+1][y1]--;
        f[x2+1][y2+1]++;
   }
    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]+f[i][j];
            cout<<f[i][j]<<" ";
        }
        cout<<endl;
    }
    
}
相关推荐
kvo7f2JTy2 小时前
基于机器学习算法的web入侵检测系统设计与实现
前端·算法·机器学习
List<String> error_P2 小时前
蓝桥杯最后几天冲刺:暴力大法(一)
算法·职场和发展·蓝桥杯
流云鹤3 小时前
Codeforces Round 1090 (Div. 4)
c++·算法
wljy14 小时前
第十三届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组(个人见解,已完结)
c语言·c++·算法·蓝桥杯·stl
清空mega5 小时前
C++中关于数学的一些语法回忆(2)
开发语言·c++·算法
香蕉鼠片5 小时前
数据结构八股(一)
数据结构·算法
Mr_Xuhhh5 小时前
从理论到实践:深入理解算法的时间与空间复杂度
java·开发语言·算法
6Hzlia5 小时前
【Hot 100 刷题计划】 LeetCode 42. 接雨水 | C++ 动态规划与双指针题解
c++·算法·leetcode
地平线开发者5 小时前
智能驾驶感知算法的演进
算法·自动驾驶