003.拓扑排序

拓扑排序

给定一张有向无环图,求出关于顶点的一个排序\(A\),满足:对于图中的每个有向边\((x,y)\)\(x\)\(A\)中都出现在\(y\)之前,则称\(A\)是该图顶点的一个拓扑序

拓扑排序可以

  • 判断有向图中是否有环
  • 生成拓扑序列

Kahn算法

\(e[x]\)存点\(x\)的邻接点,\(tp\)存拓扑序列,\(din[x]\)存点\(x\)的入度

算法的核心:用队列\(q\)维护一个入度为\(0\)的节点的集合

  • 初始化,队列\(q\)压入所有入度为\(0\)的点

  • 每次从\(q\)中取出一个点\(x\)放入数组\(tp\)

  • 然后将\(x\)的所有出边删除。若将边\((x,y)\)删除后,\(y\)的入度变为\(0\),则将\(y\)压入\(q\)

  • 不断重复上述两个过程,直到队列为空

  • \(tp\)中的元素个数等于\(n\),则有拓扑序,否则,有环

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int N = 1e5 + 10;

vector<int>e[N], tp;
int din[N], n, m;

bool topo_sort()
{
	queue<int> q;
	for (int i = 1; i <= n; i++)
	{
		if (din[i] == 0) q.push(i);
	}
	while (q.size())
	{
		int x = q.front();
		q.pop();
		tp.push_back(x);
		for (auto y : e[x])
		{
			din[y]--;
			if (din[y] == 0) q.push(y);
		}
	}
	return tp.size() == n;
}


int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b;
		cin >> a >> b;
		e[a].push_back(b);  //单向边
		din[b]++;           //入度
	}

	if (!topo_sort()) puts("-1");  //有环
	else for (auto x : tp) cout << x << " ";  //无环
	return 0;
}

时间复杂度\(O(n+m)\)


DFS算法

\(e[x]\)\(x\)的邻接点,\(tp\)存拓扑序列,\(c[x]\)存点\(x\)的颜色

算法的核心是变色法:一路搜一路给点变色,如果有拓扑序,每个点的颜色会经过\(0\rightarrow -1 \rightarrow 1\)三次变色

  • 初始状态,所有点染色为\(0\)
  • 枚举每个点,进入\(x\)点,把\(x\)点染色为\(-1\)。然后枚举\(x\)的儿子\(y\)
    • 如果\(y\)的颜色为\(0\),说明没有访问过该点,继续深搜
    • 如果\(y\)的颜色为\(-1\),说明回到了祖先节点,发现了环,一路返回false,退出程序
  • 如果枚举完\(x\)的儿子,没有发现环,把\(x\)染色为\(1\),并把\(x\)压入\(tp\)数组
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>  
using namespace std;
const int N = 1e5 + 10;

vector<int>e[N], tp;
int c[N], n, m;

// 深度优先搜索函数,用于检查图中是否存在环,并进行拓扑排序
bool dfs(int x)
{
    c[x] = -1;  // 标记当前节点为正在访问
    for (int y : e[x])  // 枚举 x 的所有邻接节点
    {
        if (c[y] == -1) return false; // 发现环,返回 false
        else if (c[y] == 0)       // 若邻接节点未被访问
        {
            if (!dfs(y)) return false; // 递归搜索邻接节点的子节点
        }
        // c[y] = 1 表示该节点已经访问过,不需要额外操作
    }

    c[x] = 1;  // 标记当前节点为已访问
    tp.push_back(x);  // 将当前节点加入拓扑排序结果中
    return true;
}

// 拓扑排序函数,调用 dfs 函数对图进行遍历
bool topo_sort()
{
    memset(c, 0, sizeof c);  // 初始化所有节点的访问状态为未访问
    for (int x = 1; x <= n; x++)
    {
        if (c[x] == 0)
            if (!dfs(x)) return false; // 若发现环,返回 false
    }
    reverse(tp.begin(), tp.end());  // 反转拓扑排序结果,得到正确顺序
    return true;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
    {
        int a, b;
        cin >> a >> b;
        e[a].push_back(b);  // 添加单向边
    }

    if (!topo_sort()) cout << "-1" << endl;  // 有环,输出 -1
    else {
        for (auto x : tp) cout << x << " ";  // 无环,输出拓扑排序结果
        cout << endl;
    }
    return 0;
}

时间复杂度\(O(n+m)\)


习题

B3644 【模板】拓扑排序 / 家谱树 - 洛谷 (luogu.com.cn)

题目描述

有个人的家族很大,辈分关系很混乱,请你帮整理一下这种关系。给出每个人的后代的信息。输出一个序列,使得每个人的后辈都比那个人后列出。

输入格式

\(1\) 行一个整数 \(N\)\(1 \le N \le 100\)),表示家族的人数。接下来 \(N\) 行,第 \(i\) 行描述第 \(i\) 个人的后代编号 \(a_{i,j}\),表示 \(a_{i,j}\)\(i\) 的后代。每行最后是 \(0\) 表示描述完毕。

输出格式

输出一个序列,使得每个人的后辈都比那个人后列出。如果有多种不同的序列,输出任意一种即可。

输入输出样例 #1
输入 #1
5
0
4 5 1 0
1 0
5 3 0
3 0
输出 #1
2 4 5 3 1
AC代码
//B3644 【模板】拓扑排序 / 家谱树
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int N = 105;
vector<int> e[N], tp;
int din[N], n, c;

void topo_sort()
{
	queue<int> q;
	for (int i = 1; i <= n; i++)
	{
		if (din[i] == 0) q.push(i);
	}

	while (q.size())
	{
		int x = q.front();
		q.pop();
		tp.push_back(x);
		for (auto y : e[x])
		{
			din[y]--;
			if (din[y] == 0) q.push(y);
		}
	}
}


int main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		while (cin >> c && c)
		{
			e[i].push_back(c);
			din[c]++;
		}
	}

	topo_sort();

	for (int i : tp) cout << i << " ";

	return 0;
}

P1137 旅行计划 - 洛谷 (luogu.com.cn)

题目描述

小明要去一个国家旅游。这个国家有 \(N\) 个城市,编号为 \(1\)\(N\),并且有 \(M\) 条道路连接着,小明准备从其中一个城市出发,并只往东走到城市 \(i\) 停止。

所以他就需要选择最先到达的城市,并制定一条路线以城市 \(i\) 为终点,使得线路上除了第一个城市,每个城市都在路线前一个城市东面,并且满足这个前提下还希望游览的城市尽量多。

现在,你只知道每一条道路所连接的两个城市的相对位置关系,但并不知道所有城市具体的位置。现在对于所有的 \(i\),都需要你为小明制定一条路线,并求出以城市 \(i\) 为终点最多能够游览多少个城市。

输入格式

第一行为两个正整数 \(N, M\)

接下来 \(M\) 行,每行两个正整数 \(x, y\),表示了有一条连接城市 \(x\) 与城市 \(y\) 的道路,保证了城市 \(x\) 在城市 \(y\) 西面。

输出格式

\(N\) 行,第 \(i\) 行包含一个正整数,表示以第 \(i\) 个城市为终点最多能游览多少个城市。

输入输出样例 #1
输入 #1
5 6
1 2
1 3
2 3
2 4
3 4
2 5
输出 #1
1
2
3
4
3
说明/提示

均选择从城市 \(1\) 出发可以得到以上答案。

  • 对于 \(20\%\) 的数据,\(1\le N ≤ 100\)
  • 对于 \(60\%\) 的数据,\(1\le N ≤ 1000\)
  • 对于 \(100\%\) 的数据,\(1\le N ≤ 100000\)\(1\le M ≤ 200000\)
AC代码

思路:节点\(i\)的深度由他的前驱节点转移而来

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int N = 1e5 + 5;
vector<int> e[N];
int din[N], deep[N], n, m;

void topo_sort()
{
	queue<int> q;
	for (int i = 1; i <= n; i++)
	{
		if (din[i] == 0) deep[i] = 1, q.push(i);
	}
	while (q.size())
	{
		int x = q.front();
		q.pop();
		for (int y : e[x])
		{
			din[y]--;
			deep[y] = max(deep[y], deep[x] + 1);
			if (din[y] == 0)
			{
				q.push(y);
			}
		}
	}
}

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b;
		cin >> a >> b;
		e[a].push_back(b);
		din[b]++;
	}
	topo_sort();
	for (int i = 1; i <= n; i++) cout << deep[i] << endl;
	
	return 0;
}
posted @ 2025-06-16 20:08  _P_D_X  阅读(18)  评论(0)    收藏  举报