图论----拓扑排序

文章目录

前置知识

在了解拓扑排序前,我们需要了解以下前置知识:

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已访问,此时把它放入栈中,最后栈中的序列就是拓扑序列。

关于拓扑排序的相关习题

最大食物链计数
杂务
最长路

后续有时间会更新题解。

相关推荐
算AI16 小时前
人工智能+牙科:临床应用中的几个问题
人工智能·算法
hyshhhh18 小时前
【算法岗面试题】深度学习中如何防止过拟合?
网络·人工智能·深度学习·神经网络·算法·计算机视觉
码农幻想梦18 小时前
第八章 图论
图论
鹭天18 小时前
【网络流 && 图论建模 && 最大权闭合子图】 [六省联考 2017] 寿司餐厅
图论
杉之19 小时前
选择排序笔记
java·算法·排序算法
烂蜻蜓19 小时前
C 语言中的递归:概念、应用与实例解析
c语言·数据结构·算法
我要昵称干什么19 小时前
基于S函数的simulink仿真
人工智能·算法
AndrewHZ19 小时前
【图像处理基石】什么是tone mapping?
图像处理·人工智能·算法·计算机视觉·hdr
念九_ysl19 小时前
基数排序算法解析与TypeScript实现
前端·算法·typescript·排序算法