题解:AcWing 848 有向图的拓扑序列
【题目来源】
AcWing:848 有向图的拓扑序列 - AcWing题库
【题目描述】
给定一个\(n\)个点\(m\)条边的有向图,点的编号是\(1\)到\(n\),图中可能存在重边和自环。请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出\(-1\)。
若一个由图中所有点构成的序列\(A\)满足:对于图中的每条边\((x,y)\),\(x\)在\(A\)中都出现在\(y\)之前,则称\(A\)是该图的一个拓扑序列。
【输入】
第一行包含两个整数\(n\)和\(m\)。
【输出】
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。否则输出\(-1\)。
【输入样例】
3 3
1 2
2 3
1 3
【输出样例】
1 2 3
【核心思想】
-
问题分析:给定有向图 \(G=(V,E)\),其中 \(|V|=n\),\(|E|=m\)。需要找到一个拓扑序列,使得对于每条有向边 \((x,y) \in E\),\(x\) 在序列中都出现在 \(y\) 之前。如果图中存在环,则拓扑序列不存在。这是一个经典的拓扑排序问题,关键在于利用入度为 \(0\) 的节点性质进行 BFS。
-
算法选择:
- Kahn 算法(BFS 拓扑排序):基于贪心思想,每次选择入度为 \(0\) 的节点加入拓扑序列
- 入度数组:维护每个节点的入度 \(d[i]\),表示指向节点 \(i\) 的边数
- 邻接表存储:使用链式前向星或 vector 存储图结构,高效遍历邻接点
-
关键步骤:
- 初始化:读取 \(n\)(节点数)、\(m\)(边数),构建邻接表和入度数组 \(d[1..n]\)
- 入队初始化:遍历所有节点 \(i\) 从 \(1\) 到 \(n\),将入度 \(d[i] = 0\) 的节点加入队列 \(q\)
- BFS 处理(当队列非空时循环):
- 取出队头节点 \(t = q[hh++]\)
- 遍历 \(t\) 的所有邻接点 \(j\):
- 删除边 \(t \to j\),即 \(d[j]--\)
- 若 \(d[j] == 0\),将 \(j\) 加入队列 \(q[++tt] = j\)
- 判断结果:
- 若队列中处理了 \(n\) 个节点(\(tt == n-1\)),输出队列中的拓扑序列 \(q[0..n-1]\)
- 否则图中存在环,输出 \(-1\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m)\),每个节点和每条边都被访问一次
- 空间复杂度:\(O(n + m)\),邻接表存储图需要 \(O(n + m)\),队列和入度数组需要 \(O(n)\)
-
拓扑排序的核心思想:
- 入度为 \(0\) 的节点优先:入度为 \(0\) 的节点没有前驱约束,可以安全地加入拓扑序列
- 删边降度:处理一个节点后,删除其所有出边,降低邻接点的入度
- 环检测:若图中存在环,则环上所有节点的入度都 \(\geq 1\),无法被加入队列
- BFS 贪心:每次选择当前可处理的节点(入度为 \(0\)),保证序列的合法性
- 适用于任务调度、依赖关系处理、编译顺序确定等场景
【解题思路】

【解题思路2】
拓扑排序,有向图

【算法标签】
拓扑排序
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100010; // 最大节点数
int n; // 节点数量
int m; // 边数量
int h[N]; // 邻接表头数组
int e[N]; // 边的终点数组
int ne[N]; // 下一条边数组
int idx; // 边的索引
int q[N]; // 队列,用于存储拓扑序列
int d[N]; // 入度数组,d[i]表示节点i的入度
// 添加有向边 a->b
void add(int a, int b)
{
e[idx] = b; // 存储边的终点
ne[idx] = h[a]; // 新边指向原来的第一条边
h[a] = idx; // 更新头指针指向新边
idx++; // 边索引增加
}
// 拓扑排序函数
// 返回值:true - 存在拓扑序列,false - 存在环
bool topsort()
{
int hh = 0; // 队列头指针
int tt = -1; // 队列尾指针
// 第一步:将所有入度为0的节点加入队列
for (int i = 1; i <= n; i++)
{
if (!d[i]) // 入度为0
{
q[++tt] = i; // 入队
}
}
// 第二步:BFS处理队列中的节点
while (hh <= tt) // 队列不为空
{
int t = q[hh++]; // 出队
// 遍历节点t的所有邻接点
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接点j
d[j]--; // 删除边t->j,j的入度减1
// 如果j的入度变为0,加入队列
if (d[j] == 0)
{
q[++tt] = j;
}
}
}
// 第三步:判断是否所有节点都被处理
// 如果队列中有n个节点,说明存在拓扑序列
return tt == n - 1;
}
int main()
{
// 输入节点数和边数
cin >> n >> m;
// 初始化邻接表
memset(h, -1, sizeof(h));
// 读入m条有向边
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
add(a, b); // 添加边 a->b
d[b]++; // b的入度加1
}
// 执行拓扑排序
if (topsort())
{
// 输出拓扑序列
for (int i = 0; i < n; i++)
{
printf("%d ", q[i]);
}
puts(""); // 换行
}
else
{
// 存在环,无法进行拓扑排序
puts("-1");
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
const int N = 100010; // 定义节点和边的最大规模
vector<int> h[N]; // 邻接表,存储图的边
int n, m, ind[N], q[N]; // n 节点数,m 边数,ind[i] 节点 i 的入度,q 队列
// 拓扑排序函数
bool topsort()
{
int hh = 0, tt = 0; // 队列的头和尾指针
// 将所有入度为 0 的节点加入队列
for (int i = 1; i <= n; i++)
if (ind[i] == 0) q[tt++] = i;
// 处理队列中的节点
while (hh < tt)
{
int t = q[hh++]; // 取出队头节点
// 遍历当前节点的邻接节点
for (int i = 0; i < h[t].size(); i++)
{
int j = h[t][i]; // 邻接节点 j
ind[j]--; // 将邻接节点的入度减 1
if (ind[j] == 0) q[tt++] = j; // 如果入度为 0,加入队列
}
}
// 如果队列中的节点数等于 n,说明存在拓扑序
return n == tt;
}
int main()
{
cin >> n >> m; // 输入节点数和边数
// 输入每条边,构建邻接表和入度数组
while (m--)
{
int a, b;
cin >> a >> b;
h[a].push_back(b); // 添加边 a -> b
ind[b]++; // 节点 b 的入度加 1
}
// 进行拓扑排序
if (topsort())
{
// 输出拓扑序
for (int i = 0; i < n; i++) cout << q[i] << " ";
}
else
{
// 如果不存在拓扑序,输出 -1
cout << -1;
}
return 0;
}
【运行结果】
3 3
1 2
2 3
1 3
1 2 3
浙公网安备 33010602011771号