1. 简介
差分和上一篇的前缀和算法一样,是经典的用空间换时间的方法,它的核心思想也是预处理。其实,差分和前缀和是一对互逆的运算(差分的基础上进行前缀和可以得到原数组)
差分也分为一维差分和二维差分,不过它们的处理步骤相差无几。都分为两步:1. 创建差分数组;2. 处理目标区间 3.还原原始数组
2. 一维差分
2.1 模板
1. 套用公式创建差分数组:f[i] =a[i]-a[i-1] (此处,原数组为 a ,差分数组为 f ,其实差分数组存储的就是元素的差值,即 f[i] 表示:原数组第 i 项和第 i-1 项的差值)
也可以根据差分数组的性质:f[i]+=a[i],f[i+1]-=a[i] (其实很好理解,当处理位置 i 时,i 与 i-1 位置的差增加 a[i];而 i 与 i+1 位置的差减少 a[i] )
2. 处理目标区间:假设将原数组 [L,R] 统一加上一个数 c : f[L]+=c,f[R+1]-=c (L 与 L-1 位置的差增加 c;而 R 与 R+1 位置的差减少 a[i])
3. 还原原始数组 a :对差分数组【前缀和】
2.2 海底高铁
2.2.1 题目描述
一条铁路经过 N 个城市,每个城市都有一个站。乘车经过两个相邻的城市之间(方向不限),要单独购买这一小段的车票。第 i 段铁路连接了城市 i 和城市 i+1(1≤i<N)。第 i 段铁路购买纸质单程票需要 Ai 元。
还有一种方案就是买 IC 卡。对于第 i 段铁路,需要花 Ci 元购买一张 IC 卡,然后乘坐这段铁路一次就只要扣 Bi(Bi<Ai) 元。每张卡都可以充值任意数额。对于第 i 段铁路的 IC 卡,无法乘坐别的铁路的车。
如果一位乘客要去 M 个城市,从城市 P1 出发分别按照 P1,P2,P3,⋯,PM 的顺序访问各个城市,可能会多次访问一个城市,且相邻访问的城市位置不一定相邻,而且不会是同一个城市。
现在他希望知道,出差结束后,至少会花掉多少的钱,包括购买纸质车票、买卡和充值的总费用
输入描述:
第一行两个整数,N,M。(N,M<=1e5)
接下来一行,M 个数字,表示 Pi。
接下来 N−1 行,表示第 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 算法分析
目标是求花费最小,就是求每段铁路 【买票花费】和 【买卡花费】 的最小值。
设每段铁路被乘坐次数为 f[i] ,买票花费为 a[i] , 工本费 c[i] ,买卡后每次花费 b[i]
【买票花费】:f[i] * a[i]
【买卡花费】:f[i]*b[i]+c[i]
我们要求的就是 f[i] ,而题中根据顺序访问,对于访问 pi ~ pj,那 [pi,pj-1] 的所有铁路都会被乘坐一次。相当于这个一段区间都加上一个数,可以用差分数组记录,最后对差分数组前缀和,就可以得到每段铁路的次数。(城市访问顺序可能 pi > pi+1,此时需要交换)
2.2.3 代码实现
#include<iostream>
using namespace std;
//数据范围会超出int
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
//差分数组
LL f[N];
int main()
{
cin >> n >> m;
//利用差分数组的性质标记次数
//从 left -> right
int left; cin >> left;
for (int i = 2; i <= m; i++)
{
int right; cin >> right;
//如果城市访问顺序 left > right,此时需要交换
if (left < right)
{
f[right]--;
f[left]++;
}
else
{
f[right]++;
f[left]--;
}
left = right;
}
//前缀和得到每段铁路次数
for (int j = 1; j <= n; j++)
{
f[j] = f[j - 1] + f[j];
}
LL ret = 0;
for (int i = 1; i < n; i++)
{
LL a, b, c; cin >> a >> b >> c;
//选出两种方案的较小值
ret += min(a * f[i], c + b * f[i]);
}
cout << ret << endl;
return 0;
}
3. 二维差分
3.1 模板
可以类比一维差分
1. 根据【差分矩阵的性质】创建差分矩阵:
这里【差分矩阵的性质】可以类比【一维差分数组的性质】:
a. 差分矩阵中求前缀和可以还原出原始数组
b. 差分矩阵中标记某个位置,后续元素要修改
举个例子:假如我们要将原始数组 a(n*n大小) 中,(x1,y1)为左上角,(x2,y2)为右下角的子矩阵每个元素加 K;
那么:将 (x1,y1) 加 k 后,如果求前缀和,那从 (x1,y1) 到 (n,n),每个位置的前缀和都要加 k;为了消除这个影响,我们要将(x1,y2+1) 和 (x2+1,y1)位置都减一个 k , (x2+1,y2+1) 加上一个 k ,这样,求前缀和时,只有 (x1,y1) 到 (x2,y2) 被影响
这样,我们可以得到差分矩阵的性质:
f[x1][y1]+=k
f[x1][y2+1]-=k
f[x2+1][y1]-=k
f[x2+1][y2+1]+=k
2. 根据性质处理目标矩阵
3. 还原原始矩阵:对 差分矩阵 前缀和
3.2 地毯
3.2.1 题目描述
在 n×n 的格子上有 m 个地毯。
给出这些地毯的信息,问每个点被多少个地毯覆盖。
输入描述:
第一行,两个正整数 n,m。(n,m<=1000)
接下来 m 行,每行两个坐标,代表一块地毯,左上角是 (x1,y1),右下角是 (x2,y2)。
输出描述:
输出 n 行,每行 n 个正整数。
第 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
3.2.2 算法分析
根据题意,我们很容易分析出这是一个【二维差分】问题,每铺一个地毯,就是在差分矩阵中得到左上角为(x1,y1) 和右下角 (x2,y2)的子矩阵 ,根据我们得出的差分矩阵的性质处理即可,我们只需要根据还原出的原始数组位置上的值,就可以知道铺多少个地毯
3.2.3 代码实现
#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
void push(int x1, int y1, int x2, int y2)
{
//差分矩阵的性质
f[x1][y1] += 1;
f[x1][y2 + 1] -= 1;
f[x2 + 1][y1] -= 1;
f[x2 + 1][y2 + 1] += 1;
}
int main()
{
int n, m; cin >> n >> m;
for (int i = 1; i <= m; i++)
{
//用差分处理每张地毯
int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
push(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] + f[i][j];
cout << f[i][j] << " ";
}
cout << endl;
}
return 0;
}