前言
前缀和与差分是算法优化的重要思想,差分可以看做是前缀和的逆运算,上集已经介绍前缀和的相关部分,接下来介绍差分相关部分。
一、差分是什么?
类似数学中的指数与对数,差分可以看成是前缀和的逆运算。
二、差分
作用
差分的作用和前缀和是一样的,都是降低算法的复杂度,简化计算。上一章前缀和求区间和a[l]+…+a[r]的复杂度为o(1),这章是通过差分改变区间值a[l] + … +a[j]也只需o(1)的复杂度。
前缀和:o(1)求区间和
差分:o(1)改变区间值
原数组a[i]: a[1]、a[2]、 … a[i]
差分数组b[i]:b[i] 、b[2] … b[i]
关系: 原数组a[i]是差分数组b[i]的前缀和,即a[i] = b[1] + b[2] +… + b[i]
“差分数组b[i]如何构造”: 反向构造 或者 先初始化为零进行区间[l,r]操作后求差分数组前缀和再与原数组进行相加。
b[1] = a[1]
b[2] = a[2] - a[1]
b[3] = a[3] - a[2]
...
b[i] = a[i] - a[i -1]
左边相加 = b[1] + b[2] +.. +b[i]
右边相加 = a[i],即可证该构造合理。
差分数组b[i]有什么用: o(1)改变区间值
问题:
给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c , a[r] + c;
暴力做法和前缀和那章一样,m次for循环遍历数组a[n],时间复杂度变成O(n*m)。
差分法:
令b[l] = b[l] + c,则根据关系a[l] = b[1] + b[2] +… b[l],则
新数组a[i]中的a[l] = a[l] + c,同理a[l]后面的各项都加上c,即a[l + 1] = a[l+1] + c,a[l + 2] = a[l+1] + c…a[i] = a[i] +c,所以原来数组a[i]就变为:a[1] 、a[2]…a[l]+c、 a[l+1] +c … a[r]+c 、 a[r+1] +c … a[i] +c.
再令b[r + 1] = b[r] -c,则数组a[r] = a[r] - c,同时后面各项都减去c,则最新数组变为:a[1] 、a[1] …a[l]+c 、a[l + 1] +c … a[r] +c、 a[r+1] …a[i] ,即为题目所求,然而该方法时间复杂度为o(n+m)
例题:
输入一个长度为n的整数序列。
接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数n和m。第二行包含n个整数,表示整数序列。
接下来m行,每行包含三个整数l,r,c,表示一个操作。
输出格式
共一行,包含n个整数,表示最终序列。
数据范围
1≤n,m≤1000001≤n,m≤100000,
1≤l≤r≤n1≤l≤r≤n,
−1000≤c≤1000−1000≤c≤1000,
−1000≤整数序列中元素的值≤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
代码一:
#include <iostream>
using namespace std;
int main(){
int n,m,l,r,c;//数组长度、询问次数、left边界、right边界,count
cin>>n>>m;
int a[n+1] = {0}; //初始化原数组
int f[n+1] = {0};//构造差分数组
for(int i = 1;i <= n;i++){
cin>>a[i];
f[i] = a[i] - a[i-1];
}
for(int i = 1;i <= m;i++){
cin>>l>>r>>c;
f[l] += c;
f[r+1] -= c; //差分数组对区间[l,r]操作
}
for(int i = 1;i <= n;i++){
f[i] += f[i-1];//对差分数组前缀和
std::cout <<f[i] <<" ";
}
return 0;
}
代码二:
#include <iostream>
using namespace std;
int main(){
int n,m,l,r,c;//数组长度、询问次数、left边界、right边界,count
cin>>n>>m;
int a[n+1]; //从1开始赋值,方便理解
int f[n+1];//构造差分数组
int sum[n+1];//差分前缀和
for(int i = 1;i <= n;i++){
cin>>a[i];
f[i] = 0;//初始化差分数组
}
for(int i = 1;i <= m;i++){
cin>>l>>r>>c;
f[l] += c;
f[r+1] -= c;
for(int j = 1;j <= n;j++){
sum[j] = sum[j-1] + f[j];//差分数组前缀和
}
}
for(int i = 1;i <= n;i++){
std::cout <<a[i] + sum[i] <<" ";//原数组 + 差分数组前缀和
}
return 0;
}
三、二维差分
作用: o(1)改变子矩阵值(面积)
原数组a[i][j]
差分数组b[i][j]
关系: 原数组a[i][j]是差分数组b[i][j]的二维前缀和
目标:给子矩阵中的每个元素都加上数c,即
已知原数组a中被选中的子矩阵为 以(x1,y1)为左上角,以(x2,y2)为右下角所围成的矩形区域;
for(int i = x1; i <= x2;i++){
for(int j = y1;j <= y2; j++){
a[i][j] += c;
}
}
差分方法实现目标:
b[x1][y1] += c;
b[x1][y2+1] -= c;
b[x2+1][y2] -= c;
b[x2+1][y2+1] += c;
差分方法实现目标函数封装:
void insert(int x1,int y1,int x2,int y2,int c)
{ //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
b[x1][y1]+=c;
b[x2+1][y1]-=c;
b[x1][y2+1]-=c;
b[x2+1][y2+1]+=c;
}
二维差分操作的构造,公式如下:
① b[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1]
② 使用insert(i,j,i,j,a[i][j])函数进行构造差分数组
如何理解通过insert()构造差分数组?
insert(i,j,i,j,a[i][j])表示对b数组执行插入操作,等价于对a数组中的(i,j)到(i,j)之间的元素(实际是单个小方格,也就是原数组初始化时的赋值操作)加上了a[i][j],
即原数组a[i][j]初始化成功,差分数组通过insert()初始化刚好对应起来.
如何理解差分方法能实现子矩阵中每个元素加上数c呢,请看图
同上图,二维差分实现子矩阵各元素加数c解析图
练习题目:
输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1, y1, x2, y2, c,其中(x1, y1)和(x2, y2)表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上c。
请你将进行完所有操作后的矩阵输出。
输入格式
第一行包含整数n,m,q。
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含5个整数x1, y1, x2, y2, c,表示一个操作。
输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。
数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2
代码一: 差分数组全部初始化为零,求差分前缀和后再与原数组相加就是最终答案了
#include<iostream>
using namespace std;
const int N=1e3+10; //1010
int a[N][N],b[N][N] = {0};
void insert(int x1,int y1,int x2,int y2,int c){
// 表示(x1,y1)---(x2,y2)的小矩阵范围中都加上c
b[x1][y1]+=c;
b[x1][y2+1] -= c;
b[x2+1][y1] -= c;
b[x2+1][y2+1] += c;
}
int main()
{
int n,m,q;//分别表示n行m列q个操作
cin>>n>>m>>q;
for(int i = 1;i <= n;i++){
for(int j =1;j <= m;j++){
cin>>a[i][j];
}
}
int x1,y1,x2,y2,c;
while(q--){
cin>>x1>>y1>>x2>>y2>>c;
insert(x1,y1,x2,y2,c);//改变原数组a[i][j]的小矩阵块值 + c
}
for(int i = 1;i <= n;i++){
for(int j = 1 ; j <= m;j++){
b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1];//求差分数组的二维前缀和
}
}
for(int i = 1;i <= n;i++){
for(int j =1;j <= m;j++){
cout<<a[i][j] + b[i][j]<<" ";//输出
}
cout<<endl;
}
return 0;
}
代码二: 通过insert函数初始化差分数组
#include<iostream>
using namespace std;
const int N=1e3+10; //1010
int a[N][N],b[N][N];
void insert(int x1,int y1,int x2,int y2,int c){
// 表示(x1,y1)---(x2,y2)的小矩阵范围中都加上c
b[x1][y1]+=c;
b[x1][y2+1] -= c;
b[x2+1][y1] -= c;
b[x2+1][y2+1] += c;
}
int main()
{
int n,m,q;//分别表示n行m列q个操作
cin>>n>>m>>q;
for(int i = 1;i <= n;i++){
for(int j =1;j <= m;j++){
cin>>a[i][j];
insert(i,j,i,j,a[i][j]);//通过insert函数巧妙构造差分数组b[i][j]
}
}
int x1,y1,x2,y2,c;
while(q--){
cin>>x1>>y1>>x2>>y2>>c;
insert(x1,y1,x2,y2,c);//改变原数组a[i][j]的小矩阵块值 + c
}
for(int i = 1;i <= n;i++){
for(int j = 1 ; j <= m;j++){
b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1];//求差分数组的二维前缀和
}
}
for(int i = 1;i <= n;i++){
for(int j =1;j <= m;j++){
cout<<b[i][j]<<" ";//输出
}
cout<<endl;
}
return 0;
}
四、三维差分
一旦到三维甚至更高维,其几何空间很难想象。下面给出三维差分实现子体积中每个小体积加上数h的相关公式。
三维差分8个式子
二进制表示(方便记忆)
规则:B(X,Y,Z)括号里面的"+"位置由二进制的"1"位置决定
加减运算符号由二进制总数"1"决定,若为奇数就(-)、为偶数就(+)
000 B(X1, Y1, Z1) += C ——使该立方体全部加上常数C
001 B(X1, Y1, Z2 + 1) -=C
010 B(X1, Y2 + 1, Z1) -= C
011 B(X1, Y2 +1, Z2 + 1) += C
100 B(X2 +1, Y1, Z1) -= C
101 B(X2 + 1, Y1, Z2 + 1) += C
110 B(X2 + 1, Y2 + 1, Z1) += C
111 B(X2 + 1, Y2 + 1, Z2 + 1) -= C
三维差分方法封装:
void insert(int x1,int y1,int z1,int x2,int y2,int z2,int h)
{//对b数组执行插入操作,等价于对a数组中的(x1,y1,z1)到(x2,y2,z2)之间的元素都加上了h
b(x1, y1, z1) +=h
b(x1, y1, z2+1) -=h
b(x1, y2+1, z1) -=h
b(x1, y2+1, z2+1) +=h
b(x2+1, y1, z1) -=h
b(x2+1, y1, z2+1) +=h
b(x2+1, y2+1, z1) +=h
b(x2+1, y2+1, z2+1) -=h
}