求解排队问题
假设我们现在要对这个图表示的排队问题进行求解:
我们考虑能否让 2 第一个进入队列。观察上图可以发现,有一条 1→2 的有向边,表示 2 进入队列之前需要先让 1 进入队列。所以 2 不能第一个进入队列。
通过上面的判断方式,我们发现可以根据一个点的入边来判断这个点进入队列前需要哪些点已经进入队列。
拓扑排序就是依照这个原理进行的,但是拓扑排序将这个判断过程简化为了对入度的判断。通过只判断入度,可以在保证正确性的情况下简化算法流程。
拓扑排序
对于一个有向图,拓扑排序的运算流程如下:
- 找到一个入度为 0 的顶点,将这个顶点加入到拓扑序的尾部。
- 删除步骤 1 中找到的点以及以该点作为起点的所有有向边。
- 回到步骤 1 ,直到步骤 1 找不到满足要求的顶点为止。
算法中提到了拓扑序这一名词,实际上拓扑序的定义与题目要求的排队序列定义相同,就是指满足该图要求的一种序列。
算法的终止有两种情况:
- 该图变成了空图,没有顶点存在。此时我们会得到一个正确的拓扑序
- 该图不是空图,但是找不到入度为 0 的结点。此时我们可以确定这个图中存在至少一个有向环,不存在拓扑序。
所以,拓扑排序不仅可以用于求解拓扑序,也可以用于判断一个有向图中是否存在有向环。
接下来我们模拟运行一下拓扑排序在下图中的运行情况:
初始化:
拓扑序 : []
顶点编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
入度 | 0 | 1 | 2 | 1 |
第 1 次迭代:
拓扑序 : [1]
顶点编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
入度 | 被删除 | 0 | 1 | 1 |
第 2 次迭代:拓扑序 : [1,2]
顶点编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
入度 | 被删除 | 被删除 | 0 | 1 |
第 3 次迭代:拓扑序 : [1,2,3]
顶点编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
入度 | 被删除 | 被删除 | 被删除 | 0 |
第 4 次迭代:拓扑序 : [1,2,3,4]
顶点编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
入度 | 被删除 | 被删除 | 被删除 | 被删除 |
第 5 次迭代:
没有找到符合要求的顶点,算法退出。
最终的拓扑序即为: [1,2,3,4]
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10010;
const int maxm = 100010;
vector<int> G[maxn];
int rd[maxn];
void addEdge(int u, int v) {
G[u].push_back(v);
rd[v]++;
}
int seq[maxn];
bool toposort(int n) {
queue<int> q;
for(int i = 1; i <= n; i++){
if (rd[i] == 0){
q.push(i);
}
}
int ncnt = 0;
while(!q.empty()){
int u= q.front();
q.pop();
seq[++ncnt] = u;
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i];
rd[v]--;
if (rd[v] == 0){
q.push(v);
}
}
}
return ncnt == n;
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
int u, v;
for (int i = 1; i <= m; i++) {
scanf("%d%d", &u, &v);
addEdge(u, v);
}
if (!toposort(n)) {
puts("Not DAG");
} else {
for (int i = 1; i <= n; i++) {
printf("%d%c", seq[i], i == n ? '\n' : ' ');
}
}
return 0;
}