这里给大家总结一下拓扑排序的几种写法或变形:用栈、用队列、基于宽搜、基于深搜、915-23 真题中的改编 topsort 求 level(DAG的最大高度)。
1.基于宽搜的topsort
下面这段代码使用拓扑排序来解决有向图的问题。拓扑排序是一种对有向无环图进行排序的算法,其基本思想是通过顶点之间的有向边的方向来排序,保证在排序结果中,任意一对有向边的起点在终点之前。
具体步骤如下:
- 统计每个节点的入度(in-degree),入度表示有多少条边指向该节点。
- 将入度为0的节点加入队列。
- 对队列中的节点进行遍历,将其邻接节点的入度减1。如果邻接节点入度为0,则加入队列。
- 重复步骤3,直到队列为空。
- 判断拓扑排序是否成功,即队列中的节点个数是否等于图的节点个数。如果等于,说明有合法的拓扑序列。
如果拓扑排序成功,输出拓扑排序的结果;否则,输出"-1"表示图中存在环,无法进行拓扑排序。
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int d[N]; // 入度数组,记录每个节点的入度
int q[N], hh = 0, tt = -1; // 队列,hh 表示队头,tt 表示队尾
struct Node
{
int id;
Node *next;
Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息
void add(int a, int b)
{
Node *p = new Node(b);
p->next = head[a];
head[a] = p;
}
bool bfs()
{
// 将入度为 0 的节点加入队列
for (int i = 1; i <= n; i++)
if (!d[i])
q[++tt] = i;
while (hh <= tt)
{
int t = q[hh++];
for (Node *p = head[t]; p; p = p->next)
{
int j = p->id;
if (--d[j] == 0)
q[++tt] = j;
}
}
// 如果队列中的节点个数等于图的节点个数,说明拓扑排序成功
return tt == n - 1;
}
int main()
{
cin >> n >> m;
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
d[b]++; // 记录每个节点的入度
}
if (!bfs())
puts("-1");
else
{
// 输出拓扑排序的结果
for (int i = 0; i < n; i++)
cout << q[i] << ' ';
cout << endl;
}
return 0;
}
2.将队列改为栈进行拓扑排序
有同学说将队列改为栈后再进行拓扑排序输出的是逆拓扑序列,这句话是错的,这里用代码给大家验证一下。
将队列改为栈之后要做的改变:需要新增拓扑排序的结果数组res来保存每次出栈元素。
其余过程一样。最后输出结果仍为拓扑有序序列。
下图来自《数据结构 C语言版 第2版 (严蔚敏)》
#include <iostream>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int d[N];
int stk[N], top = -1; // 用栈存储入度为0的节点
int res[N], k;
struct Node
{
int id;
Node *next;
Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息
void add(int a, int b)
{
Node *p = new Node(b);
p->next = head[a];
head[a] = p;
}
// 拓扑排序
bool topsort()
{
// 将入度为0的节点入栈
for (int i = 1; i <= n; i++)
if (!d[i])
stk[++top] = i;
while (top > -1)
{
int t = stk[top--];
res[k++] = t; // 将当前节点加入结果数组
for (Node *p = head[t]; p; p = p->next)
{
int j = p->id;
if (--d[j] == 0)
stk[++top] = j; // 将入度为0的相邻节点入栈
}
}
return k == n; // 如果结果数组中节点个数等于图中节点个数,说明拓扑排序成功
}
int main()
{
cin >> n >> m;
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
d[b]++; // 统计每个节点的入度
}
if (!topsort())
puts("-1"); // 无法进行拓扑排序,图中有环
else
{
for (int i = 0; i < n; i++)
cout << res[i] << ' ';
cout << endl;
}
return 0;
}
3.915-23 真题中的改编 topsort 求 level(DAG的最大高度)
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int d[N]; // 入度数组,记录每个节点的入度
int q[N], hh = 0, tt = -1; // 队列,hh 表示队头,tt 表示队尾
int level[N]; // 存每个结点的level
struct Node
{
int id;
Node *next;
Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息
void add(int a, int b)
{
Node *p = new Node(b);
p->next = head[a];
head[a] = p;
}
bool bfs()
{
// 将入度为 0 的节点加入队列
for (int i = 1; i <= n; i++)
if (!d[i])
q[++tt] = i;
while (hh <= tt)
{
int t = q[hh++];
for (Node *p = head[t]; p; p = p->next)
{
int j = p->id;
if (--d[j] == 0)
{
// level 为指向该结点的所有结点中最大的 level + 1
level[j] = max(level[j], level[t] + 1);
q[++tt] = j;
}
}
}
// 如果队列中的节点个数等于图的节点个数,说明拓扑排序成功
return tt == n - 1;
}
int main()
{
cin >> n >> m;
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
d[b]++; // 记录每个节点的入度
}
if (!bfs())
puts("-1");
else
{
// 输出拓扑排序的结果
for (int i = 0; i < n; i++)
cout << q[i] << ' ';
cout << endl;
// 输出level的结果
for (int i = 1; i <= n; i++)
cout << level[i] << ' ';
cout << endl;
}
return 0;
}
4.基于深搜的topsort
4.1求逆拓扑有序序列
该代码的前提是保证图存在拓扑序列。
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
bool st[N]; // 标记每个点是否被访问过
int d[N]; // 入度数组,记录每个节点的入度
struct Node
{
int id;
Node *next;
Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息
void add(int a, int b)
{
Node *p = new Node(b);
p->next = head[a];
head[a] = p;
}
void dfs(int u)
{
st[u] = true; // 将当前结点标记为已访问
// 遍历当前结点的邻接点
for (Node *p = head[u]; p; p = p->next)
{
int j = p->id; // 邻接点编号
if (!st[j]) // 若该点没访问过
dfs(j);
}
cout << u << ' '; // 退出递归时输出
// 此时得到逆拓扑有序序列
}
int main()
{
cin >> n >> m;
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
}
dfs(1);
return 0;
}
4.2深度优先遍历判断图中是否有环的算法如下:
-
选择一个起始节点,将其标记为"正在访问"状态。
-
检查当前节点的所有相邻节点:
-
如果相邻节点为"未访问"状态,则递归地进行深度优先搜索这个节点
- 如果在搜索过程中发现这个节点已经被标记为"正在访问",则说明存在环路,返回true
-
如果相邻节点状态为"已访问",忽略该节点
-
-
所有相邻节点均处理完成后,将当前节点标记为"已访问"状态,并返回上层递归
-
如果整个图都搜索完,没有返回true,则说明图中不存在环路,返回false。
主要思路是:
-
使用"未访问"、"正在访问"和"已访问"三种节点状态进行标记
-
如果搜索某个节点时发现其状态为"正在访问",则说明从此节点开始一定存在一个环路
-
整个深度优先搜索结束没有返回true,则该图不存在环路
通过遍历每个节点及其相邻节点,并检查其访问状态就可以判断是否存在环路。时间复杂度为O(V+E),空间复杂度为O(V)。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m, q[N], top;
int st[N]; // 0表示没有遍历过,1表示在递归当中,2表示已经遍历完了
struct Node
{
int id;
Node* next;
Node(int _id) : id(_id), next(NULL){}
} *head[N];
void add(int a, int b)
{
Node* p = new Node(b);
p->next = head[a]; // 头插法
head[a] = p;
}
bool dfs(int u) // 如果有环的话返回false
{
st[u] = 1; // 标记当前点在递归中
for (Node* p = head[u]; p; p = p->next)
{
int j = p->id;
if (!st[j]) // 如果没有搜过的话(0)就直接搜
{
if (!dfs(j)) return false; // 如果搜的过程中出现了环,直接false
}
else if (st[j] == 1) return false; // 否则如果发现这个点在递归当中,直接false
}
q[top ++] = u;
st[u] = 2; // 标记当前点已经遍历完了
return true;
}
// 基于深搜的话要遍历所有点,因为这个图不一定是联通的
bool topsort()
{
for (int i = 1; i <= n; i ++)
if (!st[i] && !dfs(i)) // 如果没遍历的话就遍历它,如果发现有环的话直接false
return false;
return true;
}
int main()
{
cin >> n >> m;
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
}
if (topsort())
{
// 因为得到的是拓扑排序的逆序序列,所以要倒序输出
for (int i = n - 1; i >= 0; i -- ) cout << q[i] << ' ';
cout << endl;
}
else puts("-1");
return 0;
}
以上内容来自【xjtu915 冲刺班】