拓扑排序算法
文章目录
一、前言
今天是拓扑排序算法~
二、基础知识
- DAG图:Directed Acyclic Graph,有向无环图
- AOV网络:Activity On Vertex Network,用顶点表示活动,用弧表示活动之间的优先关系的有向图称为顶点表示活动的网
三、拓扑排序算法
3.1 定义
拓扑序列 :设G = (V, E)是一个具有n个顶点的有向图,V中的顶点序列V1、V2、V3...Vn满足若从顶点Vi->Vj有一条路径,则在顶点序列中顶点Vi必在Vj之前。我们称其为拓扑序列
拓扑排序:对DAG图构造拓扑序列的过程
只有无环图才能产生拓扑序列
3.2 应用
-
得到拓扑序列
-
有向图判环
-
关键路径(拓扑排序做中间步骤)
-
找做事的先后顺序
-
分层( K a h n Kahn Kahn算法找入度为0的点就可以把层分出来)

3.3 算法
3.3.1 kahn(卡恩)算法
基本思想
基于BFS
流程
- 在有向图中选一个没有前驱节点的顶点输出
- 从图中删除该顶点和所有以它为尾的弧
- 循环上述两步
具体实现
- 建图:建图过程中维护每个顶点的入度
- 初始:将入度为0的点全部入队
- 队非空,则将队首顶点出队并输出,然后将该顶点的邻接点的入度减1。若邻接点入度为0,则邻接点入队
- 队空则结束
- 可以用一个变量计算输出顶点的个数,若全部输出则无环,否则有环
数据结构中使用的是栈,在这里是使用队列~
代码
cpp
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#include<queue>
#define LL long long
#define PII pair<int,int>
#define maxn 5005
using namespace std;
int n, m; // n个点,m条边
// 链式前向星存图,注意边不带权值
struct edge
{
int v, next;
// v是邻接点,next下一条边的编号
} e[maxn];
int cnt; // 边集数组的下标
int head[maxn];
// 入度数组
int ind[maxn];
int topo[maxn], k;
// k是下标
void add(int x,int y)
{
cnt++;
e[cnt].v=y;
e[cnt].next=head[x];
head[x]=cnt;
}
// 把环也判出来
bool kahn()
{
int u,v;
queue<int> q;
for(int i=1;i<=n;i++)
{
if(ind[i]==0)
{
q.push(i);
}
}
while(!q.empty())
{
u=q.front();
q.pop();
k++;
topo[k]=u;
for(int i=head[u];i!=-1;i=e[i].next)
{
v=e[i].v;
ind[v]--;
if(ind[v]==0)
{
q.push(v);
}
}
}
if(k==n)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int x,y;
memset(head,-1,sizeof(head));
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d %d",&x,&y);
add(x,y);
ind[y]++;// y的入度++
}
if(kahn()==1)
{
for(int i=1;i<=k;i++)
{
printf("%d ",topo[i]);
}
}
else
{
printf("含环\n");
}
return 0;
}
/*
输入
5 5
1 2
2 4
3 2
3 4
4 5
输出
1 3 2 4 5
*/
时间复杂度
O ( v + e ) O(v+e) O(v+e)
链式前向星存图,遍历了所有的顶点,以及后面指向的边
3.3.2 基于DFS的算法
基本思想
使用 D F S DFS DFS算法遍历图,并且在回溯的时候将遍历的顶点入栈,那么先入栈的顶点必定入度不为0,而入度为0的顶点必定是最后入栈。最后将栈自顶向下输出,即为拓扑序列。
- D F S DFS DFS和 B F S BFS BFS跑出来的序列不唯一,所以拓扑序列也不唯一
- 栈可以模拟递归,但在这里不是,只是为了存储
时间复杂度
O ( v + e ) O(v+e) O(v+e)
如何判环
对于某个节点,在搜索过程中给与三种状态:0 1 2
有环:一个顶点会访问两次
具体流程
- 在每一轮的搜索开始时,我们任取一个[未搜索]的节点开始进行深度优先搜索。当搜索到当前节点u时,我们将其置为[搜索中],遍历其相邻节点v。
- 如果v为[未搜索],我们开始搜索v,直到搜索回溯到u
- 如果v为[搜索中],说明存在环,不存在拓扑序列
- 如果v为[已完成],说明v已经在栈中了,对结果不影响,不用进行任何操作
当所有的节点都被搜索完成后,如果没有找到环,那么栈中存储的节点,从栈顶到栈底的顺序即为拓扑序列
为何用DFS找拓扑序列
基于这些顶点在拓扑序列中的相对位置和在 D F S DFS DFS中的相对位置是一样的
代码
cpp
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#include<stack>
#define LL long long
#define PII pair<int,int>
#define maxn 5005
using namespace std;
int n,m; // n个点,m条边
// 链式前向星存图,注意边不带权值
struct edge
{
int v,next;
// v是邻接点,next下一条边的编号
}e[maxn];
int cnt; // 边集数组的下标->边的数目
int head[maxn];
stack<int> s;
int vis[maxn]; // 状态数组
int flag=0;// 无环
void add(int x,int y)
{
cnt++;
e[cnt].v=y;
e[cnt].next=head[x];
head[x]=cnt;
}
void dfs(int u)
{
vis[u]=1;// u处于搜索中
int v;
for(int i=head[u];i!=-1;i=e[i].next)
{
v=e[i].v;
if(vis[v]==1) // 访问过v了,但它没有入栈,说明有环
{
flag=1; // 有环
return;
}
else if(vis[v]==0)
{
dfs(v);
if(flag==1) // 判断dfs的过程是否有环
{
return;
} // 进行剪枝
}
}
vis[u]=2;
s.push(u);
// return;
}
int main()
{
int x,y;
memset(head,-1,sizeof(head));
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d %d",&x,&y);
add(x,y);
}
for(int i=0;i<=n;i++)
{
if(vid[i]==0)
{
dfs(i);
}
}
if(flag==0)
{
while(!s.empty())
{
printf("%d ",s.top());
s.pop();
}
}
else
{
printf("含有环\n");
}
return 0;
}
/*
输入
5 5
1 2
2 4
3 2
3 4
4 5
输出
3 1 2 4 5
*/
从任意点开始进行遍历都没问题,因为正着找的序列不是拓扑序列,回退的过程才是拓扑序列~
3.4 题目
3.4.1 洛谷
- P1113 [USACO02FEB] 杂务
- P1983 [NOIP 2013 普及组] 车站分级
四、小结
关于算法的介绍就到这里啦~ 快去刷题吧~(PS:这种算法的题目往往比较难懂)