一维差分
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目只有题目分析,代码实现,代码误区
从这篇算法开始,精炼了算法内容,减少了冗余解释
题目描述:
-
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。输出格式
共一行,包含 n 个整数,表示最终序列。数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2 -
题目来源:https://www.acwing.com/problem/content/799/
题目分析:
- 给定一个序列,多次统一修改序列片段,只要求输出最终序列
- 暴力:
m次操作,每次遍历l到r的元素为其加c
最坏时间复杂度:O(mn),当每次l=0,r=n-1时
m最大十万,n最大十万,mn最大百亿,比拼多多补贴还大,严重超时 - 一维差分:
我们已经学习过了一维前缀和(传送门),了解了前缀和的打表公式,差分是这个公式的高级应用
非常适合解决多次加减修改,只输出最终结果的题目
下面介绍差分,注意理解差分的时间复杂度是O(n)
算法原理:
含义:
- 差分是什么?
已知前缀和数组s[] = {1, 3, 6, 10, 15};
则原数组arr[] = {1, 2, 3, 4, 5};
原数组arr[] 称为 前缀和数组s[] 的差分数组 - 差分数组对前缀和数组的影响:
现在我们将差分数组的arr[1]+1,之后前缀和数组变为s[] = {1, 4, 7, 11, 16};
再将差分数组的arr[4]-1,则前缀和数组变为:s[] = {1, 4, 7, 11, 15};
我们改动了差分数组的两个值arr[1] 和 arr[4],影响了前缀和数组的s[1] 到 s[3]的区间
作用:
- 差分数组改动一点,前缀和数组改动一个区间
- 题目要求:改动l - r的区间
现在将题目数组视为前缀和数组,求得其差分数组之后,化区间变化为两点变化,时间复杂度大大降低,从O(n)到O(2) - 解题只需两步:1,求差分数组 2,改动差分数组两点 3,求前缀和还原回原数组
求差分公式:
法一:打表
- 已知前缀和打表公式:s[i] = s[i-1] + arr[i];
- 则差分打表公式:arr[i] = s[i] - s[i-1];
- 当然求差分是个过程,最终目的是改动差分两点后还原回前缀和数组(已经将题目数组视作前缀和数组)
法二:插入
- 差分数组的改动影响前缀和数组的结果
- 已知前缀和数组中arr[3] = 4;
可以想象这个4也是由差分插入的:
原本s[i]和arr[i]均为0
后来改动arr[3]+=4;arr[4]-=-4;
则前缀和数组中只有s[3]受波动影响变为4,s[4]开始又是0 - 所以已知前缀和数组中一点s[i],可以还原到差分数组对应两点arr[i] & arr[i+1]
- 插入公式:arr[i] += s[i]; & arr[i+1] -= s[i];
差分区间波动公式:
- 欲将前缀和数组中l 到 r 的每一点都+c
需要修改差分数组中两点:arr[l] += c; & arr[r+1] -= c; - 注意原数组加减范围是l & r,差分数组修改两点是l & r+1
存储形式:
- 一维数组即可,为了方便求差分,差分数组和前缀和数组仍然从1开始存储
写作步骤:
3步
1. 由原前缀和数组求差分数组
2. 修改差分数组中两点
3. 求改动后的前缀和数组
代码模板:
const int N = 100010;
int arr[N];
int s[N];
void insert(int l, int r, int c){
arr[l] += c;
//注意点3:从r+1开始修剪
arr[r+1] -= c;
}
int main(){
int n=0, m=0;
//注意点1:求题目数组的差分,则题目数组是前缀和数组
//注意点2:下标都是从1开始
for(int i=1; i<=n; i++) cin>> s[i];
for(int i=1; i<=n; i++) insert(i,i,s[i]);
/*也可以打表出差分数组
for(int i=1; i<=n; i++) arr[i] = s[i] - s[i-1];
*/
for(int i=0; i<m; i++){
int l = 0, r = 0, c = 0;
cin >>l >>r >>c;
insert(l, r, c);
}
for(int i=1; i<=n; i++) s[i] = s[i-1]+arr[i];
/*差分数组之后不用了,可以直接用差分数组求最终结果数组
for(int i=1; i<=n; i++) arr[i] += arr[i-1];
*/
for(int i=1; i<=n; i++) cout<<s[i];
}
代码误区:
1. 为什么输入数组是前缀和数组,而不是差分数组?
- 题目输入一个数组后,要求统一改动数组区域
- 差分数组改动两点后,前缀和数组改动一个区域
- 所以为了降低时间复杂度,我们将输入数组视作前缀和数组,求其差分数组,对差分数组进行两点修改后,累加得到最终前缀和数组
2. 求差分数组有几种方法?
法一:差分打表公式:
- arr[i] = s[i] - s[i-1];
法二:差分波动公式/插入法:
- 由一点s[i],可以推测arr[i]和arr[i+1]
- 由全部的s[],可以确定全部arr[]
- 已知s[i] = a; 则arr[i] += a; arr[i+1]-=a;
3. 已知差分数组有几种方法求前缀和数组?
法一:前缀和打表公式:
- s[i] = s[i-1] + arr[i];
法二:差分迭代:
- arr[i] += arr[i-1];
4. 差分法适用的运算:
- 本题是加减法
- 换一个乘除,开方,乘方就不可
- 因为前缀和和差分只有加减线性关系
本篇感想:
- 今天的第一篇博客下午三点才出炉,焯!
- 快了,马上就要到练气境的中期了,坚持
- 看完本篇博客,恭喜已登《练气境-初期》
距离登仙境不远了,加油