文章目录
前置知识
在了解拓扑排序前,我们需要了解以下前置知识:
1.入度:对于一个有向图,一个顶点的入度是指以该顶点为终点的有向边的数目。
2.出度:对于一个有向图,一个顶点的出度是指以该顶点为起点的有向边的数目。
3.有向无环图(DAG):在图中,如果任意一个顶点在经过若干条有向边后都不能回到该点,那么它就是一个有向无环图。
什么是拓扑排序
在图论领域,有一个用于处理有向无环图(DAG)的重要方法,那就是拓扑排序。它的主要目的是把图里的所有顶点排列成一个线性序列,使得对于图中任意一条有向边 A → B,在这个序列里顶点 A 一定出现在顶点 B 的前面。简单来讲,就是要确定事件的执行顺序,保证所有的依赖关系都能得到满足。
当然要注意的时,拓扑序列可能不唯一。
拓扑排序的算法思想
Kahn算法
1.统计每个点的入度,将入度为0的顶点放入队列。
2.从队列中取出一个顶点,输出该顶点或者将其放入拓扑序列,将该顶点的出边删除,对该顶点的邻接点的入度减一。
3.重复步骤2,直到队列为空。如果图中还有未输出的顶点,则说明图中存在环,无法进行拓扑排序。(因为图中如果存在环的话,那么总会有顶点的入度不为0,也就无法放入队列进行拓扑排序)
DFS算法
深度优先遍历:通过递归的 DFS 遍历图中的每个顶点,处理其所有邻接顶点后再处理当前顶点
状态标记:使用vis数组标记顶点状态:
0:未访问
1:正在访问(在递归栈中)
2:已完成访问
逆序入栈:在 DFS 回溯时将顶点压入栈中,最终栈的顺序即为逆序的完成时间
执行流程:
初始化:所有顶点标记为 0(未访问)
遍历所有顶点:确保处理所有连通分量
递归 DFS:
标记当前顶点为 1(正在访问)
遍历所有邻接顶点:
若邻接顶点正在访问(状态 1),则存在环
若未访问,递归处理邻接顶点
处理完所有邻接顶点后,标记当前顶点为 2,并压入栈
输出结果:若检测到环则输出提示,否则按栈顺序输出顶点
拓扑排序的代码实现
Kahn算法
cpp
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
int n, m;
int ind[105];
vector<int>p[105];
queue<int>q;
vector<int>topu;
int main() {
cin >> n >> m;
int x, y;
while (m--) {
cin >> x >> y;//添加x->y的有向边
p[x].push_back(y);
ind[y]++;
}
for (int i = 1; i <= n; i++) {
if (ind[i] == 0)q.push(i);
}
while (!q.empty()) {
int u = q.front();
topu.push_back(u);
q.pop();
for (int i = 0; i < p[u].size(); i++) {
int v = p[u][i];
ind[v]--;
if (ind[v] == 0)q.push(v);
}
}
if (topu.size() != n) {
cout << "存在环,无法进行拓扑排序" << endl;
}
return 0;
}
当然这里除了用队列实现,还可以用栈来实现。我们来看看区别:
队列实现拓扑排序
队列遵循先进先出(FIFO)的原则。在拓扑排序中使用队列时,会先将入度为 0 的节点依次加入队列。之后,每次从队列头部取出一个节点进行处理,把该节点从图中移除(即减少其所有邻接节点的入度),若有邻接节点的入度变为 0 就将其加入队列尾部。这样的操作能保证按照节点入度为 0 的先后顺序依次处理节点,实现了一种层次化的排序,也就是广度优先搜索(BFS)的思想。
栈实现拓扑排序
栈遵循后进先出(LIFO)的原则。在拓扑排序里使用栈时,同样先把入度为 0 的节点压入栈。然后,每次从栈顶取出一个节点处理,移除该节点并更新其邻接节点的入度,若邻接节点入度变为 0 就将其压入栈。虽然栈的操作顺序和队列不同,但它依然能保证对于任意有向边 A → B,A 会在 B 之前被处理。因为只有当一个节点的所有前驱节点都被处理完(入度变为 0),它才会被加入栈中等待处理,所以最终也能得到一个合法的拓扑排序序列。
这里用栈和队列都可以得到拓扑序列,但是得到的序列顺序可能不同,例如:
对于边集 {(1,2), (1,3), (2,4), (3,4)}:
队列结果:1 → 2 → 3 → 4(按层处理)
栈结果:1 → 3 → 2 → 4(优先处理新入栈的节点)
DFS算法
cpp
#include<iostream>
#include<cstring>
#include<stack>
using namespace std;
struct edge{
int v, next;
}e[10005];
int head[105];
stack<int>s;
int n, m;
int cnt;
int vis[105];
void add(int x, int y) {
e[cnt].v = y;
e[cnt].next = head[x];
head[x] = cnt;
cnt++;
}
int flag;
void dfs(int u) {
vis[u] = 1;
int v;
for (int i = head[u]; i != -1; i = e[i].next) {
v = e[i].v;
if (vis[v] == 1) {
flag = 1;
return;
}
else if(!vis[v]) {
dfs(v);
if (flag == 1) {
return;
}
}
}
vis[u] = 2;
s.push(u);
}
int main() {
cin >> n >> m;
memset(head, -1, sizeof(head));
int x, y;
while (m--) {
cin >> x >> y;
add(x, y);
}
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
dfs(i);
}
}
if (flag == 0) {
while (!s.empty()) {
cout << s.top() << " ";
s.pop();
}
}
else {
cout << "有环";
}
return 0;
}
这里的dfs函数内,flag是用来判断是否有环的,vis[x]==1表示点x正在被访问中,如果此时它还能作为邻接点,则说明图中存在环,vis[x]==2则说明点x已访问,此时把它放入栈中,最后栈中的序列就是拓扑序列。
关于拓扑排序的相关习题
后续有时间会更新题解。