拓扑排序详解及C++实现

本文详细介绍了拓扑排序的概念,包括有向无环图(DAG)的定义、拓扑序列的含义,以及拓扑排序的逻辑过程。通过手动模拟拓扑排序的步骤,帮助理解算法工作原理。同时,提供了C++代码实现拓扑排序,并给出了如何利用拓扑排序来检测图中是否存在环的方法。

拓扑排序详解及C++实现

定义

百度百科定义如下:

拓扑排序,是对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。

很显然这一段话不是人话十分晦涩难懂,令人深思。

有向无环图

图论基础知识可以参考图论(一)基本概念_图论是什么_翟羽嚄的博客-CSDN博客

需要注意:有向无环图不一定是树,例如:

image-20230430103758048

拓扑序列

对于一个有向无环图将图中的顶点排成一个序列,其中每个边的起点在序列中一定在终点之前;

(↑↑ 不是人话

通俗一点解释为:将一张图“压扁”,使顶点从左到右排成序列,且压扁后每条边的方向只能朝右,那么这个序列是这张图的拓扑序列。

例如这张图:

image-20230430104239518

其拓扑序列为1-2-4-3

image-20230430104626428

对于同一张图,可能存在不止一个拓扑序列。例如上图的另一个拓扑序列为1-4-2-3

image-20230430105127835

拓扑排序

那么拓扑排序可表示为:计算一个有向无环图的拓扑序列。

逻辑

拓扑排序的逻辑如下:

  1. 集合 S S S表示所有入度为0的点;队列 L L L表示拓扑序列。初始时为空。
  2. 找到所有入度为0的点,放入 S S S中。
  3. S S S中取出一个点 u u u,放入 L L L中。
  4. 在图中删除 s s s,并删除所有以 s s s为起点的边。
  5. 重复2~4,直到 S S S为空。

手动模拟拓扑排序的步骤如下:

手动模拟内容较长,如已经理解可以跳过

对于这张图:

image-20230521092839958

其中入度为0的点为1,因此 S S S L L L初始化如下:

image-20230521092458342

1 S S S中取出并放入 L L L

image-20230521103033724

删除点1和所有以1为起点的边:

image-20230521103309587

此时23的入度更新为0,所以将23放入 S S S

image-20230521103517957

重复上述步骤。将2 S S S中取出、放入 L L L;删除2和以2为起点的边;将6推入队列:

image-20230521104451245

继续重复此步骤,分别从 S S S中取3654789

image-20230521105206491

image-20230521110302990

image-20230521110416138

image-20230521110640041

image-20230521110738598

image-20230521110904746

image-20230521111005146

此时 S S S为空,所以结束计算。

拓扑序列为1 2 3 6 5 4 7 8 9

image-20230521111652089

代码实现

读入部分

使用链式向前星储存图:

struct edge
{
	int to;
	int next;
};

int head[100005];
edge graph[100005];
int n, m;

读入图:

cin >> n >> m;
for (int i = 1; i <= m; i++)
{
	int s, e;
	cin >> s >> e;

	graph[i].next = head[s];
	graph[i].to = e;
	head[s] = i;
}

由于需要计算点的入度,因此使用d数组储存节点的入度:

int d[100005];

在读入图时计算:

d[e]++;

读图部分完整代码为

#include <iostream>
using namespace std;

struct edge
{
	int to;
	int next;
};

int head[100005];
edge graph[100005];
int d[100005];
int n, m;

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int s, e;
		cin >> s >> e;

		graph[i].next = head[s];
		graph[i].to = e;
		head[s] = i;
		d[e]++;
	}
}

拓扑排序部分

使用队列que储存集合 S S S

queue<int> que;

遍历 1 ∼ N 1\sim N 1N,将入度为0(d[i] == 0)的点插入队列:

for (int i = 1; i <= n; i++)
{
	if (d[i] == 0)
	{
		que.push(i);
	}
}

que不为空时,从que中取出一个点 u u u,并输出(加入拓扑序列 L L L

while (!que.empty())
{
	int now = que.front();
	que.pop();
	cout << now << " ";
}

删除所有以 u u u为起点的边;实现时不需要真正从图中删除,直接将终点的入度减1即可:

for (int i = head[now]; i != 0; i = graph[i].next)
{
	d[graph[i].to]--;
}

如果某个点的入度更新后变为0,那么将这个点推入que

for (int i = head[now]; i != 0; i = graph[i].next)
{
	d[graph[i].to]--;
	if (d[graph[i].to] == 0)
	{
		que.push(graph[i].to);
	}
}

拓扑排序部分完整代码为:

...

queue<int> que;

...

int main()
{
	...

	while (!que.empty())
	{
		int now = que.front();
		que.pop();
		cnt--;
		cout << now << " ";
		for (int i = head[now]; i != 0; i = graph[i].next)
		{
			d[graph[i].to]--;
			if (d[graph[i].to] == 0)
			{
				que.push(graph[i].to);
			}
		}
	}
}

拓扑排序判环

上文中提到拓扑排序的只能对有向无环图进行排序。

如果图中出现环,则 S S S为空时图不为空。

利用这个特点,可以判断有向图中是否存在环。

使用cnt变量计算que中的元素数量(出队元素数量):

...
int main()
{
	...
	
	int cnt = n;

	while (!que.empty())
    ...
}

每出队一个元素,cnt--

while (!que.empty())
{
	int now = que.front();
	que.pop();
	
	cnt--;
	
	...
}

如果计算结束后图不为空,即cnt > 0,则图中有环:

if (cnt) // cnt > 0
{
	cout << "Circle!" << endl;
}

完整代码

#include <iostream>
#include <queue>
using namespace std;

struct edge
{
	int to;
	int next;
};

int head[100005];
edge graph[100005];

int d[100005];
int n, m;

queue<int> que;

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int s, e;
		cin >> s >> e;

		graph[i].next = head[s];
		graph[i].to = e;
		head[s] = i;
		d[e]++;
	}

	for (int i = 1; i <= n; i++)
	{
		if (d[i] == 0)
		{
			que.push(i);
		}
	}

	int cnt = n;

	while (!que.empty())
	{
		int now = que.front();
		que.pop();
		cnt--;
		cout << now << " ";
		for (int i = head[now]; i != 0; i = graph[i].next)
		{
			d[graph[i].to]--;
			if (d[graph[i].to] == 0)
			{
				que.push(graph[i].to);
			}
		}
	}

	cout << endl;
	if (cnt)
	{
		cout << "Circle!" << endl;
	}
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值