拓扑排序详解🔥

232 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」第3天,点击查看活动详情 拓扑排序 并不是像它的名字所说那样是类似于快速,冒泡,希尔,插入排序之类的某种排序算法。

它仅仅只是对于某种特定的图, 描述图的可执行线性结构。

这个图就是大名鼎鼎的 DAG网(Directed acyclic graph),即有向无环图

他有两个特点:

1.图的边有方向,(如果图中的每一条边都是双向的  即无向图)  
2.图中没有环

那什么叫做环呢

左图这个头追着尾巴的圆圈就是环,右图就不是环

从上面的环图中可以看出,如果每个点都代表一个事件,倘若现在要做 事件1,那我们就要先完成 事件1的前置事件2,要完成事件2,我们就要先完成 事件3才行 ,要做事件3, 就要先完成事件4,要做事件4就要完成事件1,

我们发现最后皮球又踢回了事件1身上,(这算不算解铃还须系铃人呢哈哈哈)所以呀这个做事过程就形成了一个死循环,无法正常的完成任意一件事,这就叫没有拓扑序。

还有一个重要的点,DAG图和有拓扑序之间是充分必要的证明关系。

也就是说:但凡是张DAG图,它就有拓扑序,如果有拓扑序,这张图就是一定是DAG图

🆗 我们来解释一下这个拓扑序是啥意思

假设现在要去做黄焖牛肉,在做这道菜之前,我们要穿鞋子穿衣服,带上足够的money,然后去菜市场买相应的食材,接着回到家将食材洗涤,之后可能还需要上网查一下这道菜怎么做,然后呢我们才能对食材进行处理,最后做菜。

很明显,这张图是一个很典型的有向无环图,也存在自己的拓扑序。

而这个拓扑序就是 求一种可能的做事顺序,能够让你成功做完菜


即:按照拓扑序,在每个项目开始时,能够保证它的前驱活动都已完成,从而使整个工程顺利进行。

但有的时候,某张图的拓扑序不一定是唯一的 ,比如上图中,我们可以在去完菜市场后,先进行查攻略,或者先洗菜,这两个顺序都是ok的。

在这张DAG图中,边的实际意思是事件的依赖关系,即要做下一件事时,要先把前置的事件做完。

现在我们来讲一下算法的详细执行过程。

我们首先要明白,我们最开始在找拓扑序时,是以谁为起点的。

很显然是穿鞋,穿衣,取钱这些事情对吧 ,因为他们没有要完成的前置事件

所以我们在进行寻找拓扑序时,要先找到图中入度为0的结点,即没有前置事件的事

小tip:

点的入度:指向该点的边的条数

点的出度:该点指向其他点的边的条数

对于拓扑排序而言,我们只关心点的入度情况。所以在接下来的分析中,我们暂时不考虑出度。

第一步:

预先处理出所有点的入度,找出那些入度为0的,也就是没有前置事件,可以直接做的事件。

第二步:

将上步处理出来的入度为0的结点放入一个容器中

这里我用的是queue队列来装结点。

queue队列是什么?

queue是一种先进先出的数据结构,它的一端为入口(队尾入队),另一端为出口(队首出队),

支持尾加(push),头删(pop),取头(front),查容量(size)等功能。

quque<int>qq;
qq.pop();//删除队列的头部元素
qq.push(x);//往队尾插入一个元素x
int top=qq.front();//取出队列头部元素
int size=qq.size();//获取队列元素个数

之所以用queue这种容器是因为它可以帮助我们完成接下来拓扑排序中的相关操作。

还是这张做菜图,我们把它标上序号

在这张图中,点1,2,3是没有点指向它们的,因此它们三的入度为0,

我们先把这三个点放入容器中,

第三步:

这意味着我们将这三件事可以做了,然后我们把点1,2,3从容器中分别取出来,因为点4是这三件事的后置事件,所以每做一件,指向点4的边就少一条,我们的点4入度就减一,做完三件事后,我们的 点4就没有了前置事件。也就是说,4的出度从3 变为了0. 在买菜过程中就是,穿戴取钱完之后,就成功解锁下一步啦.

所以,这个时候 点4的入度为0, 便可以放进我们的容器中了,代表它可以被做了。

同时我们需要删除之前在容器中已经完成了的点1,2,3这三件事;

然后我们将点4这件事,从容器中取出来,然后完成,并将点5,6的入度减一,这时候点5和点6就没有了前置事件,入度也变成了0

然后我们就可以把点5和点6放进容器里啦,别忘了把已经完成的点4删掉呀。

紧接着我们把容器中的点5,6取出,做完他们,与之相连的点7的入度就减2,变为了0。

最后!!我们把容器中的点5,6删除,加入入度为0的点7,再取出点7,删除点8的一个入度。

发现 点8入度为0, 将点8加入容器删除点7。然后取出点8,至此,我们发现图中已经没有跟点8

可以指向的点了,说明我们完成了整个工程。

以上就是拓扑排序的详细过程啦

算法实现:

在我们的有向无环图中,我们的建图用的是博主上篇文章讲的链式前向星, 想了解的童鞋可以去看看,

建图函数

deg数组记录的是每个点的入度

我们处理每个点入度的过程:

for(int i=1;i<=m){
	cin>>u>>v>>d;//输入一条边的起点,终点,长度
    add(u,v,d);
    deg[v]++;//只有被指向的终点入度增加
}

然后我们遍历图用的是链式前向星的配套遍历图的方法

以下代码就是我们拓扑排序的精髓了

bool top_sort(){
    int cnt=0;
    for (int i = 1; i <= n; i++)
        if (!deg[i])qq.push(i),top[++cnt]=i;
    while(qq.size()){
    	int x=qq.front();//取出队头
        qq.pop();//删除对头
        for(int i=next[x];i!=0;i=e[i].last){
        	int y=e[i].to;//得到x点连接的各个点
            deg[y]--;
            if(deg[y]==0)//如果在做完事件x后,事件y的入度为0,就把他放入容器中
                top[++cnt]=y,qq.push(y);
    }return cnt==n-1;//如果这个寻找拓扑序过程中成功的经过了n个点就是有拓扑序
}

看到最后就给你放完整代码叭。

马蜂奇特,请多见谅

最后的代码

#include<iostream>
#include<queue>
using namespace std;
int n, m, u, v, bian[232232], cnt, top[232232],deg[232232];
struct edge {
	int to, last;
}e[232232];
void add(int u, int v) {
	e[++cnt].to = v;
	e[cnt].last = bian[u];
	bian[u] = cnt;
}
bool top_sort() {
	queue<int>qq;
	int cnt = 0;
	for (int i = 1; i <= n; i++)
		if (!deg[i])qq.push(i), top[++cnt] = i;
	while (qq.size()) {
		int x = qq.front();//取出队头
		qq.pop();//删除对头
		for (int i = bian[x]; i; i = e[i].last) {
			int y = e[i].to;//得到x点连接的各个点
			deg[y]--;
			if (deg[y] == 0)//如果在做完事件x后,事件y的入度为0,就把他放入容器中
				top[++cnt] = y;
				qq.push(y);
		}
	}	return cnt == n;//如果这个寻找拓扑序过程中成功的经过了n个点就是有拓扑序
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++)
		scanf("%d%d", &u, &v), add(u, v), deg[v]++;
	if (top_sort())
		for (int i = 1; i <= n; i++)
			printf("%d ", top[i]);
	else printf( "-1") ;//如果没有拓扑序就打印-1

	return 0;
}