开启掘金成长之旅!这是我参与「掘金日新计划 · 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;
}