拓扑排序 (DFS和BFS及判断是否有环)
一、什么是拓扑排序?
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。
拓扑排序其实还是挺奇妙的,就是解决谁先谁后的问题
例如,下面这个图:
-
从 DAG 图中选择一个没有前驱(即入度为0)的顶点并输出。
-
从图中删除该顶点和所有以它为起点的有向边。
-
重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。
通常,一个有向无环图可以有一个或多个拓扑排序序列。这是因为可能同时存在多个入度为0的结点,这时,先处理哪个都是可以的。
二. 拓扑排序用来干什么?
- 判断是否有环
- 求DAG中的最长链
拓扑排序有两种方式,就是bfs和dfs,一般书中介绍的大多数是bfs,大家就以为拓扑排序只有一种办法,其实是不对的。
参考链接 :https://blog.csdn.net/weixin_43918531/article/details/86740991
三、拓扑排序的bfs模板
有一个明确的思路:每一个顶点都有入度和出度,入度为0说明没有指向他的,那么就从他开始往下找。把这个入度为0的push进队列(还要注意保存入度为0的点),同时把与这个点相连的所有点的入度-1,然后再看看有没有入度为0的,有的话继续push,循环上面的操作,直到没有入度为0的点。
看一下上面的图,如果从序号1开始的话:1入度为0,push进队列,顶点2的入度-1,所以顶点2push进队列,3和5的入度-1,3push进队列,5push进队列,顶点3进队列后,顶点4入度-1,顶点4push进队列,所以输出结果就是1 2 3 5 4
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
//本代码功能:以bfs输出一个有向无环图DAG的拓扑序
const int N = 1010;
vector<int> edge[N]; //邻接表
int ind[N]; //入度数组
queue<int> q; //队列
int n; //n个结点
int m; //m条边
int main() {
//读入,建图
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
ind[y]++;//维护入度
}
//入度为零的放入队列
for (int i = 1; i <= n; i++) if (!ind[i]) q.push(i);
//广度优先搜索DAG,就是拓扑排序的模板
while (!q.empty()) {
int x = q.front();
q.pop();
//输出拓扑序
cout << x << " ";
for (int i = 0; i < edge[x].size(); i++) { //遍历所有出边
int y = edge[x][i]; //目标结点
//对接点入度-1,抹去这条入边
ind[y]--;
//如果入度为0,则入队列,准备处理它
if (!ind[y]) q.push(y);
}
}
return 0;
}
/**
测试数据
5 4
1 2
2 3
2 5
3 4
参考答案:
1 2 5 3 4
*/
四、拓扑排序的dfs模板
DFS是从一个点不断往下递归,比如说从序号1往下递归,有箭头就一直往下进行,直到到了最后一个元素,就开始往栈里(当然也可以是vector之类的,只不过需要反向再输出)push元素。比如说上面的从序号1开始,到序号2,序号3,序号4,到尽头了,就把4push进栈中,3push进栈,这个时候由于5也是2的下一个元素,所以5push进栈中,2push进栈,1push进栈,然后输出就是1 2 5 3 4.
当然这个递归的顺序是与你输入的顺序有关的,不过思路都是这样的,由起始点向下递归。
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
//本代码功能:以dfs输出一个有向无环图DAG的拓扑序
const int N = 1010;
bool st[N]; //标识是不是已经使用过
vector<int> edge[N]; //邻接表
vector<int> res; //拓扑序列
/**
* 功能:深度优先搜索,记录拓扑序
* @param u
*/
void dfs(int u) {
//如果访问过了,则返回,不再重复访问
if (st[u])return;
//标识u结点已使用
st[u] = true;
//遍历每个出边,找到下一组结点
for (int v:edge[u]) if (!st[v]) dfs(v);
//这一层完毕才把它自己扔进去,最后扔等于最先输出,因为后面是倒序输出的
res.push_back(u);
}
int n; //n个结点
int m; //m条边
int main() {
//读入,建图
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
}
//将所有结点进行深入优先搜索
for (int i = 1; i <= n; i++) dfs(i);
//输出,从后向前输出
for (int i = res.size() - 1; i >= 0; i--)
cout << res[i] << " ";
}
/**
测试数据
5 4
1 2
2 3
2 5
3 4
参考答案:
1 2 5 3 4
*/
五、dfs判断是否有环
#include <bits/stdc++.h>
using namespace std;
/**
5 5
1 2
2 3
2 5
3 4
3 1
对比上个例子,添加了一条3->1的边,就成了有向有环图.
本题,也就不能输出拓扑序了,因为有环图没有拓扑序,拓扑序是针对DAG的。可以判断是否有环。
*/
//本代码功能:以dfs判断一个有向图SDG是否有环
const int N = 1010;
int st[N]; //标识是不是已经使用过
vector<int> edge[N];//邻接表
int n; //n个结点
int m; //m个关系
/**
* 功能:深度优先搜索,判断以u开头的图中是否有环,有环:true,无环:false
有向有环图dfs判断是否有环只需要把st[]的状态改一下,原本是两种状态,0和1,
现在改成 0,1,-1
0:代表未访问
-1:代表访问完毕
1:代表是这一阶段正在访问的(这一阶段指的是两个元素在同一个递归中)。
*/
bool dfs(int u) {
//标识u结点正在访问
st[u] = 1;
//遍历每个出边,找到下一组结点
for (int v:edge[u]) {
//如果遇到了正在访问的结点,那么说明有环
if (st[v] == 1) return true;
//如果v这个结点没有访问过,递归查找v结点是否在环中
if (st[v] == 0 && dfs(v)) return true;
}
//标识u结点访问完毕
st[u] = -1;
return false;
}
int main() {
//读入,建图
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
}
//将所有未访问过的结点进行深入优先搜索判断是否有环
for (int i = 1; i <= n; i++)
if (!st[i] && dfs(i))//没有访问过,并且有环
cout << "发现环!" << endl;
return 0;
}
六、bfs判断是否有环
#include <bits/stdc++.h>
using namespace std;
/**
5 5
1 2
2 3
2 5
3 4
3 1
对比上个例子,添加了一条3->1的边,就成了有向有环图.
本题,也就不能输出拓扑序了,因为有环图没有拓扑序,拓扑序是针对DAG的。可以判断是否有环。
*/
//本代码功能:以bfs判断一个有向图SDG是否有环
const int N = 1010;
int st[N]; //标识是不是已经使用过
vector<int> edge[N];//邻接表
int ind[N]; //入度表
int n; //n个结点
int m; //m个关系
queue<int> q; //队列
vector<int> ans; //ans 为拓扑序列
int main() {
//读入,建图
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
edge[x].push_back(y);
ind[y]++;
}
for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i); //将入度为0的点入队列
while (!q.empty()) {
int p = q.front();
q.pop(); // 选一个入度为0的点,出队列
ans.push_back(p);
for (int i = 0; i < edge[p].size(); i++) {
int y = edge[p][i];
ind[y]--;
if (ind[y] == 0) q.push(y);
}
}
if (ans.size() == n) {
for (int i = 0; i < ans.size(); i++)
printf("%d ", ans[i]);
printf("\n");
} else printf("No Answer!\n"); // ans 中的长度与n不相等,就说明无拓扑序列
return 0;
}