基础算法 —— 差分 【复习总结】

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值