拓扑排序(提升)
什么是拓扑排序?
维基百科对于拓扑排序有如下定义:
a topological sort or topological ordering of a directed graph is a linear ordering of
its vertices such that for every directed edge uv from vertex u to vertex v, u comes
before v in the ordering.
即:对于任何有向图而言,其拓扑排序为其所有结点的一个线性排序(对于同一个有向图而言可能存在多个这样的结点排序)。该排序满足这样的条件——对于图中的任意两个结点 u
和 v
,若存在一条有向边从 u
指向 v
,则在拓扑排序中 u
一定出现在 v
前面。
拓扑排序主要用来解决有向图中的依赖解析(dependency resolution)问题。
举例来说,如果我们将一系列需要运行的任务构成一个有向图,图中的有向边则代表某一任务必须在另一个任务之前完成这一限制。那么运用拓扑排序,我们就能得到满足执行顺序限制条件的一系列任务所需执行的先后顺序。当然也有可能图中并不存在这样一个拓扑顺序,这种情况下我们无法根据给定要求完成这一系列任务,这种情况称为循环依赖(circular dependency)。
拓扑排序存在的前提
当且仅当一个有向图为有向无环图(directed acyclic graph,或称DAG)时,才能得到对应于该图的拓扑排序。每一个有向无环图都至少存在一种拓扑排序。该论断可以利用反证法被证明如下:
假设我们有一由 v_1
到 v_n
这 n 个结点构成的有向图,且图中 v_1,v_2,...,v_n
这些结点构成一个环。这即是说对于所有 1≤i<n-1
,图中存在一条有向边从 v_i
指向 v_i+1
。同时还存在一条从 v_n
指向 v_1
的边。假设该图存在一个拓扑排序。
那么基于这样一个有向图,显然我们可以得知对于所有 1≤i<n-1,v_i
必须在 v_i+1
之前被遍历,也就是 v_1
必须在 v_n
之前被遍历。同时由于还存在一条从 v_n
指向 v_1
的边,v_n
必须在 v_1
之前被遍历。这里出现了与我们的假设所冲突的结果。因此我们可以知道,该图存在拓扑排序的假设不成立。也就是说,对于非有向无环图而言,其拓扑排序不存在。
拓扑排序的算法和实现
拓扑排序的问题存在一个线性时间解。也就是说,若有向图中存在 n 个结点,则我们可以在 \(O(n)\) 时间内得到其拓扑排序,或在 \(O(n)\) 时间内确定该图不是有向无环图,也就是说对应的拓扑排序不存在。
例如一个有向无环图如下:
根据图中的边的方向,我们可以看出,若要满足得到其拓扑排序,则结点被遍历的顺序必须满足如下要求:
1.结点1必须在结点2、3之前
2.结点2必须在结点3、4之前
3.结点3必须在结点4、5之前
4.结点4必须在结点5之前
则一个满足条件的拓扑排序为 [1, 2, 3, 4, 5]
。
若我们删去图中4、5结点之前的有向边,上图变为如下所示:
则我们可得到两个不同的拓扑排序结果:[1, 2, 3, 4, 5]
和 [1, 2, 3, 5, 4]
。
为了说明如何得到一个有向无环图的拓扑排序,我们首先需要了解有向图结点的入度(indegree)和出度(outdegree)的概念。
假设有向图中不存在起点和终点为同一结点的有向边。
入度:设有向图中有一结点 v
,其入度即为当前所有从其他结点出发,终点为 v
的的边的数目。也就是所有指向 v
的有向边的数目。
出度:设有向图中有一结点 v
,其出度即为当前所有起点为 v
,指向其他结点的边的数目。也就是所有由 v
发出的边的数目。
在了解了入度和出度的概念之后,再根据拓扑排序的定义,我们自然就能够得出结论:要想完成拓扑排序,我们每次都应当从入度为 0 的结点开始遍历。因为只有入度为 0 的结点才能够成为拓扑排序的起点。否则根据拓扑排序的定义,只要一个结点 v
的入度不为 0,则至少有一条边起始于其他结点而指向 v
,那么这条边的起点在拓扑排序的顺序中应当位于 v
之前,则 v
不能成为当前遍历的起点。
由此我们可以进一步得出一个改进的深度优先遍历或广度优先遍历算法来完成拓扑排序。以广度优先遍历为例,这一改进后的算法与普通的广度优先遍历唯一的区别在于我们应当保存每一个结点对应的入度,并在遍历的每一层选取入度为0的结点开始遍历(而普通的广度优先遍历则无此限制,可以从该吃呢个任意一个结点开始遍历)。这个算法描述如下:
1.初始化一个int[] inDegree保存每一个结点的入度。
2.对于图中的每一个结点的子结点,将其子结点的入度加1。
3.选取入度为0的结点开始遍历,并将该节点加入输出。
4.对于遍历过的每个结点,更新其子结点的入度:将子结点的入度减1。
5.重复步骤3,直到遍历完所有的结点。
6.如果无法遍历完所有的结点,则意味着当前的图不是有向无环图。不存在拓扑排序。
板子
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
return tt == n - 1;
}
例题
有个人的家族很大,辈分关系很混乱,请你帮整理一下这种关系。
给出每个人的孩子的信息。
输出一个序列,使得每个人的孩子都比那个人后列出。
输入格式
第 1 行一个整数 n,表示家族的人数;
接下来 n 行,第 i 行描述第 i 个人的孩子;
每行最后是 0 表示描述完毕。
每个人的编号从 1 到 n。
输出格式
输出一个序列,使得每个人的孩子都比那个人后列出;
数据保证一定有解,如果有多解输出任意一解。
数据范围
1≤n≤100
输入样例:
5
0
4 5 1 0
1 0
5 3 0
3 0
输出样例:
2 4 5 3 1
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, M = N * N / 2;
int n;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
void add (int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ )
{
int son;
while (cin >> son, son)
{
add(i, son);
d[son] ++ ;
}
}
topsort();
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
由于无敌的凡凡在2005年世界英俊帅气男总决选中胜出,Yali Company总经理Mr.Z心情好,决定给每位员工发奖金。
公司决定以每个人本年在公司的贡献为标准来计算他们得到奖金的多少。
于是Mr.Z下令召开 m 方会谈。
每位参加会谈的代表提出了自己的意见:“我认为员工 a 的奖金应该比 b 高!”
Mr.Z决定要找出一种奖金方案,满足各位代表的意见,且同时使得总奖金数最少。
每位员工奖金最少为100元,且必须是整数。
输入格式
第一行包含整数 n,m,分别表示公司内员工数以及参会代表数。
接下来 m 行,每行 2 个整数 a,b,表示某个代表认为第 a 号员工奖金应该比第 b 号员工高。
输出格式
若无法找到合理方案,则输出“Poor Xed”;
否则输出一个数表示最少总奖金。
数据范围
1≤n≤10000,
1≤m≤20000
输入样例:
2 1
1 2
输出样例:
201
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 20010;
int n, m;
int h[N], e[M], ne[M], idx;
int q[N];
int d[N];
int dist[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
return tt == n - 1;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(b, a);
d[a] ++ ;
}
if (!topsort()) puts("Poor Xed");
else
{
for (int i = 1; i <= n; i ++ ) dist[i] = 100;
for (int i = 0; i < n; i ++ )
{
int j = q[i];
for (int k = h[j]; ~k; k = ne[k])
dist[e[k]] = max(dist[e[k]], dist[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res += dist[i];
printf("%d\n", res);
}
return 0;
}
给定一张 N 个点 M 条边的有向无环图,分别统计从每个点出发能够到达的点的数量。
输入格式
第一行两个整数 N,M,接下来 M 行每行两个整数 x,y,表示从 x 到 y 的一条有向边。
输出格式
输出共 N 行,表示每个点能够到达的点的数量。
数据范围
1≤N,M≤30000
输入样例:
10 10
3 8
2 3
2 5
5 9
5 9
2 3
3 9
4 8
2 10
4 9
输出样例:
1
6
3
3
2
1
1
1
1
1
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <bitset>
using namespace std;
const int N = 30010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int d[N], q[N];
bitset<N> f[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
d[b] ++ ;
}
topsort();
for (int i = n - 1; i >= 0; i -- )
{
int j = q[i];
f[j][j] = 1;
for (int k = h[j]; ~k; k = ne[k])
f[j] |= f[e[k]];
}
for (int i = 1; i <= n; i ++ ) printf("%d\n", f[i].count());
return 0;
}
超经典例题车站分级
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, M = 1000010;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int q[N], d[N];
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
d[b] ++ ;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n + m; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if ( -- d[j] == 0)
q[ ++ tt] = j;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i ++ )
{
memset(st, 0, sizeof st);
int cnt;
scanf("%d", &cnt);
int start = n, end = 1;
while (cnt -- )
{
int stop;
scanf("%d", &stop);
start = min(start, stop);
end = max(end, stop);
st[stop] = true;
}
int ver = n + i;
for (int j = start; j <= end; j ++ )
if (!st[j]) add(j, ver, 0);
else add(ver, j, 1);
}
topsort();
for (int i = 1; i <= n; i ++ ) dist[i] = 1;
for (int i = 0; i < n + m; i ++ )
{
int j = q[i];
for (int k = h[j]; ~k; k = ne[k])
dist[e[k]] = max(dist[e[k]], dist[j] + w[k]);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, dist[i]);
printf("%d\n", res);
return 0;
}