拓扑排序与动态规划

1 拓扑排序

对于每一个有向无环图,我们都能获得它的拓扑序列。

可以理解为:有向无环图的每一个节点都是一个事件,每个事件的邻接点表示只有完成当前事件才能进行的事件。拓扑序列就是解决所有事件的顺序。

拓扑排序,就是寻找拓扑序列的算法。它能够将一个有向无环图转化成线性的方式存储。

我们需要如下概念:

  • 有向无环图(DAG):其中不存在从任何顶点出发并最终回到该顶点的路径的图。

  • 入度:一个节点指向它的边数。

拓扑排序的策略:

  1. 找到入度为 0 0 0 的节点,把它放进拓扑序列。
  2. 删除这个节点(相应地,其节点的邻接点的度数减 1 1 1)。
  3. 重复1、2步骤。

通过上面的步骤,我们可以知道:

  • 只有有向无环图才能获得完整的拓扑序列,因为当节点删除到只剩下一个环的情况时,剩余的所有节点都存在入度。
  • 拓扑序列是不一定唯一的。因为除非有特殊要求,同时有多个入度为 0 0 0 的节点,放入拓扑序列的顺序不确定。

例如,求如图的有向无环图的拓扑序列的步骤:

  1. 节点1的入度为 0 0 0,拓扑序列为1,删除节点1。
  1. 节点2的入度为 0 0 0,拓扑序列为1 2,删除节点2。
  1. 节点3的入度为 0 0 0,拓扑序列为1 2 3,删除节点3。
  1. 节点4的入度为 0 0 0,拓扑序列为1 2 3 4,删除节点4。

得到拓扑排序代码框架:

入度为0的点入队
while 队列不为空
    队头所有邻接点入度-1
    如果存在邻接点入度为0入队
    出队

出队序列即为拓扑序列。

代码实现:

  • num[i]:保存编号为 i i i 的节点的入度。
  • e[i][j]:保存编号为 i i i 的节点的第 j j j 个邻接点的编号。
vector<vector<int>> e(N);
queue<int> q;
int num[N];
void bfs() {
	for (int i = 1; i <= n; i++) if(!num[i]) q.push(i);
	while (!q.empty()) {
		int fat = q.front();
		cout << fat << " ";
		for (int i = 0; i < e[fat].size(); i++) {
			num[e[fat][i]]--;
			if (!num[e[fat][i]]) q.push(e[fat][i]);
		}
		q.pop();
	}
}

2 拓扑排序与动态规划

2.1 拓扑排序求最短路

在第二章中,求最短路(最长路)起点都是无入度的节点,实际问题中可能要改动。

我们可以在求拓扑序列代码的基础上进行修改。

定义:

  • s t e p i step_i stepi:记录到达编号为 i i i 的节点的最短路。

根据该定义,我们可以初始化为:
s t e p i = { 1 ( i 节点无入度 ) M A X ( i 节点有入度 ) step_i = \left\{ \begin{aligned} &1(i节点无入度) \\ &MAX(i节点有入度) \\ \end{aligned} \right. stepi={1(i节点无入度)MAX(i节点有入度)

在第2章中,求最长路(最短路)中的DAG点值(边权)均为1,实际问题中可能要改动。

得到 s t e p step step 的动态转移方程:
s t e p i = m i n ( s t e p i , s t e p p + 1 ) ( i 节点有入度, p 为可到达 i 节点的节点 ) step_i = min(step_i,step_p+1)(i节点有入度,p为可到达i节点的节点) stepi=min(stepi,stepp+1)(i节点有入度,p为可到达i节点的节点)

代码如下:

int n, num[N], step[N], res; //num[]为入度数组
vector<vector<int>> e(2 * N);
queue<int> q;
void bfs() {
	for (int i = 1; i <= n; i++) {
        step[i] = INT_MAX;
		if (!num[i]) {
			q.push(i);
            step[i] = 1;
		}
	}
	while (!q.empty()) {
		int fat = q.front();
		for (int i = 0; i < e[fat].size(); i++) {
			num[e[fat][i]]--;
			step[e[fat][i]] = min(step[e[fat][i]], step[fat] + 1);
			if (!num[e[fat][i]]) q.push(e[fat][i]);
		}
		q.pop();
	}
}

2.2 拓扑排序求最长路

同理。 s t e p step step 初始化为:
s t e p i = { 1 ( i 节点无入度 ) 0 ( i 节点有入度 ) step_i = \left\{ \begin{aligned} &1(i节点无入度) \\ &0(i节点有入度) \\ \end{aligned} \right. stepi={1(i节点无入度)0(i节点有入度)
s t e p step step 的动态转移方程:
s t e p i = m a x ( s t e p i , s t e p p + 1 ) ( i 节点有入度, p 为可到达 i 节点的节点 ) step_i = max(step_i,step_p+1)(i节点有入度,p为可到达i节点的节点) stepi=max(stepi,stepp+1)(i节点有入度,p为可到达i节点的节点)
代码如下:

int n, num[N], step[N], res;
vector<vector<int>> e(2 * N);
queue<int> q;
void bfs() {
	for (int i = 1; i <= n; i++) {
		if (!num[i]) {
			q.push(i);
			step[i] = 1;
		}
	}
	while (!q.empty()) {
		int fat = q.front();
		for (int i = 0; i < e[fat].size(); i++) {
			num[e[fat][i]]--;
			step[e[fat][i]] = max(step[e[fat][i]], len[e[fat][i]] + step[fat]);
			if (!num[e[fat][i]]) q.push(e[fat][i]);
		}
		q.pop();
	}
}

3 例题

3.1 例题1:食物链

题目描述:

给定 n n n 个物种和 m m m 条能量流动关系( a i → b i a_i\to b_i aibi),求其中的食物链条数。

物种的名称为从 1 1 1 n n n 编号。

单独的一种孤立生物不算一条食物链。

题解:

动态规划思想:

  • f i f_i fi :编号为 i i i 的节点可以到达的路径数量。

  • 所有入度为 0 0 0 的节点 f i = 1 f_i=1 fi=1

  • 其余节点的 f i f_i fi 为能到达它的节点的 f j f_j fj 之和。

如下图,是一个经过处理的图:

图中一共有 4 4 4 条食物链,分别是:

1 → 3 → 4 1\to 3\to 4 134 1 → 3 → 5 1\to 3\to 5 135 1 → 5 1\to 5 15 2 → 5 2\to 5 25

经观察可知, 4 = f 4 + f 5 4=f_4+f_5 4=f4+f5.所以可知食物链的数量为出度为 0 0 0 的节点的 f i f_i fi 之和。

特别地,因为“单独的一种孤立生物不算一条食物链“,所以应当进行调整:入度为 0 0 0 的节点如果有出度,则 f i = 1 f_i=1 fi=1,否则不做处理。

int n, m, num[N], ans, f[N];
vector<vector<int>> e(N);
queue<int> q;
void bfs() {
	for (int i = 1; i <= n; i++) {
		if(!num[i]) {
			q.push(i);
			if (e[i].size()) f[i] = 1; //有邻接点,即有出度,不是单独生物,fi=1
		}
	}
	while (!q.empty()) {
		int fat = q.front();
		for (int i = 0; i < e[fat].size(); i++) {
			num[e[fat][i]]--;
			f[e[fat][i]] += f[fat];  //求到达路径
			if (!num[e[fat][i]]) q.push(e[fat][i]);
		}
		if (!e[fat].size()) ans += f[fat];  //无邻接点,即无出度,ans累计路径
		q.pop();
	}
}

3.3 例题2:杂务

洛谷P1113 杂务

题目描述:

杂务的数目 n n n

对于每一个杂务 i i i,都有:

  • 工作序号(保证在输入文件中是从 1 1 1 n n n 有序递增的);
  • 完成工作所需要的时间 ;
  • 一些必须完成的准备工作,由一个数字 0 0 0 结束。

输出完成所有杂务所需的最短时间。

题解:

求最短时间:使用bfs拓扑排序即可。

考虑时间的计算:因为先完成的准备工作必须要等到后完成的准备工作全部完成之后才能继续进行后面的工作,所以考虑求最长路,其中点的值是完成工作所需要的时间。

最后,将没有出度的节点(也就是最后完成的工作)的时间求最大值,

#include <bits/stdc++.h>
#define file(FILENAME) freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define endl "\n"
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, len[N], num[N], ans[N], res;
vector<vector<int>> e(2 * N);
queue<int> q;
void bfs() {
	for (int i = 1; i <= n; i++) {
		if (!num[i]) {
			q.push(i);
			ans[i] = len[i];
		}
	}
	while (!q.empty()) {
		int fat = q.front();
		for (int i = 0; i < e[fat].size(); i++) {
			num[e[fat][i]]--;
			ans[e[fat][i]] = max(ans[e[fat][i]], len[e[fat][i]] + ans[fat]);
			if (!num[e[fat][i]]) q.push(e[fat][i]);
		}
		if (!e[fat].size()) res = max(res, ans[fat]);
		q.pop();
	}
}
int main() {
    CLOSE;
    cin >> n;
	for (int i = 1; i <= n; i++) {
		int id, work;
		cin >> id;
		cin >> len[id];
		while (cin >> work && work) {
			e[work].push_back(id);
			num[id]++;
		}
	} 
	bfs();
	cout << res;
	return 0;
}

3.3 例题3:聪明的大马猴

题目描述:

给定 n n n 块砖的长、宽、高 (可以改变砖的摆放方向),每块砖有无数个。求这些砖堆叠后的最大高度 (堆叠要求下面的砖底面积、长、宽严格大于上面的)

题解:

把每个砖的 6 6 6 种摆放方法,每种方法看做一个节点,按照堆叠的条件构造为一个DAG,拓扑排序求最长路(每个节点高累加和最大值)即可。

代码说明:

  • node a[N]:结构体保存每个节点的长、宽、高。

  • check(int key1, int key2):判断编号为 k e y 1 key_1 key1 的节点上面是否可以堆叠节点 k e y 2 key_2 key2

  • solve():构造DAG。

  • bfs():拓扑排序求最大高度。

代码如下:

#include <bits/stdc++.h>
#define file(FILENAME) freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define endl "\n"
using namespace std;
typedef long long ll;
const int N = 250;
int n, ans, size, num[N];
struct node {
	int x, y, z;
} a[N];
vector<vector<int>> e(2 * N);
bool check(int key1, int key2) { //判断
	if (a[key1].x * a[key1].y <= a[key2].x * a[key2].y) return false;
	if (a[key1].x <= a[key2].x) return false;
	if (a[key1].y <= a[key2].y) return false;
	return true;
}
void solve() {  //构图
	for (int i = 1; i <= size; i++) {
		for (int j = 1; j <= size; j++) {
			if (check(i, j)) {
				e[i].push_back(j);   //存边
				num[j]++;
			}
		}
	}
}
queue<int> q;
int step[N];
void bfs() {   //拓扑排序
	for (int i = 1; i <= size; i++) {
		if(!num[i]) {
			q.push(i);
			step[i] = a[i].z;
		}
	}
	while (!q.empty()) {
		int fat = q.front();
		for (int i = 0; i < e[fat].size(); i++) {
			num[e[fat][i]]--;
			if (!num[e[fat][i]]) {
				q.push(e[fat][i]);
			}
			step[e[fat][i]] = max(step[fat] + a[e[fat][i]].z, step[e[fat][i]]);
		}
		q.pop();
	}
}
int main() {
	while (scanf("%d", &n) && n) {
		ans = 0, size = 0;
		memset(num, 0, sizeof num);
		memset(step, 0, sizeof step);
		for (int i = 1; i <= n; i++) {
			int x, y, z;
			scanf("%d%d%d", &x, &y, &z);
			a[++size].x = x, a[size].y = y, a[size].z = z;   //存点
			a[++size].x = x, a[size].y = z, a[size].z = y;
			a[++size].x = y, a[size].y = x, a[size].z = z;
			a[++size].x = y, a[size].y = z, a[size].z = x;
			a[++size].x = z, a[size].y = x, a[size].z = y;
			a[++size].x = z, a[size].y = y, a[size].z = x;
		}
		solve();
		bfs();
		for (int i = 1; i <= size; i++) {
			ans = max(ans, step[i]);
			e[i].clear();
		}
		printf("%d\n", ans);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值