定义
对一个有向无环图 (Directed Acyclic Graph 简称 DAG) G 进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v,若边<u,v>∈ E(G),则 u 在线性序列中出现在 v 之前。通常,这样的线性序列称为满足拓扑次序 (Topological Order) 的序列,简称拓扑序列
由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序(topological-sort,别称 topsort),由于图的邻接表更合适稀疏图,因此下面的算法实现都是邻接表存储图
举例
以我们平时起床的穿衣顺序为例。如下图1,秋裤,裤子,腰带的顺序是一定的;袜子和鞋子的顺序是一定的;衬衫,领带,夹克的顺序是一定的;至于手表,我们随时都可以戴,因此并不影响我们穿其他的衣服。在保证每一块的顺序一定的情况下,我们也可以穿秋裤之后穿袜子,然后穿衬衫,但是裤子肯定不能排在秋裤前面,鞋子也肯定不能排在裤子或者袜子前面(如果有奇葩当我没说)。图二是其中一个拓扑序列,我们平时的个人穿衣顺序都是拓扑序列
我们可以观察到,拓扑序列并不唯一,但是拓扑序列的起点肯定是入度为0的点
BFS拓扑排序
实现思路1:无前驱节点优先
- 在有向图中选一个无前驱的顶点并且输出
- 从图中删除所有和它有关的边
- 重复上述两步,直至所有顶点输出。如果还有顶点未输出,则说明有环
queue<int> q;
vector<int> G[N];
vector<int> ans; //存放拓扑序列
int in[N];
bool bfs_topsort(int n){ //n为节点个数
for(int i=1;i<=n;i++)
if(in[i]==0) q.push(i); //将入度为0的点入队列
while(!q.empty()){
int u=q.front();
q.pop(); //选一个入度为 0 的点,出队
ans.push_back(u);
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
in[v]--;
if(in[v]==0) q.push(v); //当某一条边的入度为0,入队
}
}
if(ans.size()==n){
for(int i=0;i<ans.size();i++)
printf("%d%c",ans[i],i==ans.size()-1?'\n':' ');
return true;
}
return false;
}
注意:如果需要输出和字典序相关的拓扑序列,那么就把队列替换为优先队列即可
实现思路二:无后继节点优先
与无前驱的顶点优先相反,依次取出度为 0(无后继)的节点加入队列,每次入队都将入边删除。
使用此方法得到的序列是拓扑排序的逆序
DFS拓扑排序
DFS 总是沿着一条路搜索到底,然后逐层回退,因此 DFS也适合拓扑排序,并且我们可以在回退的过程中保存下拓扑排序的逆序
注意,此时需要将所有的节点都DFS一遍才能有最终的拓扑序列
#include <iostream>
#include <vector>
using namespace std;
const int N=1e5+10;
vector<int> G[N];
int vis[N]; //1代表正在访问,-1代表访问结束,0代表未访问
int ans[N],tot;
bool dfs_topsort(int u){ //从节点u出发
vis[u]=1; //正在访问
for (int i=0;i<G[u].size();i++){
int v=G[u][i];
if(vis[v]==1) return false; //如果后继比前驱先访问,说明存在有向环
if(!vis[v]&&!dfs_topsort(v)) return false; //如果后继未被访问,访问后继返回假,也是失败
}
vis[u]=-1;
ans[tot++]=u; //在递归结束才加入拓扑序列数组中,最深层次先返回
return true;
}
int main()
{
int n,m,a,b;
cin>>n>>m;
while(m--){
cin>>a>>b;
G[a].push_back(b);
}
for(int i=1;i<=n;i++)
if(!vis[i]) dfs_topsort(i);
for(int i=tot-1;i>=0;i--)
cout<<ans[i]<<" ";
return 0;
}
拓展
1.如何判断拓扑序列是否唯一?
答:如果在某一节点在判断所有边时,入队节点数目大于1,那么肯定不唯一。再考虑第一次入队,如果第一次也有多个节点入队那么肯定不唯一。综上,如果当前队列的大小大于1,那么拓扑序列肯定不唯一,否则唯一
//判断拓扑序列是否唯一
bool bfs_topsort(int n){
for(int i=1;i<=n;i++)
if(in[i]==0) q.push(i);
while(!q.empty()){
if(q.size()>1) return false;
int u=q.front();
q.pop();
ans.push_back(u);
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
in[v]--;
if(in[v]==0) q.push(v);
}
}
return true;
}
2.如何求出所有的拓扑序列?
联系全排列的知识,我们可以按拓扑排序规定的顺序输出所有的拓扑序列
类似DFS求全排列,我们在其基础上添加表示每个点的入度的数组,那么就是每次for循环只有没访问过且入度为0的节点更新到答案数组中,每次dfs之后都将标志清空,将入度回复,这样和全排列一样最后可以生成按字典序由小到大的拓扑序列
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int maxn=?;
int n,m;
vector<int> G[maxn]; //邻接表存储图
int ans[maxn],in[maxn];
bool vis[maxn];
void dfs_all_topsort(int x){ //x初始传入1
if(x==n+1){
for(int i=1;i<=n;i++) printf("%d%c",ans[i],i==n?'\n':' ');
return;
}
for(int i=1;i<=n;i++){
if(!vis[i]&&!in[i]){
ans[x]=i;
vis[i]=1;
for(int j=0;j<G[i].size();j++) in[G[i][j]]--;
dfs_all_topsort(x+1); //继续深层遍历
//由底层开始逐层返回需要恢复初始状态
vis[i]=0;
for(int j=0;j<G[i].size();j++) in[G[i][j]]++;
}
}
}
int main()
{
int a,b;
scanf("%d%d",&n,&m);
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++) G[i].clear();
while(m--){
scanf("%d%d",&a,&b);
G[a].push_back(b);
in[b]++;
}
dfs_all_topsort(1);
return 0;
}
我们还可以发现,如果把恢复操作省略,那么得到的就是一个拓扑序列,因此该方法也作为求任意拓扑序列的一个较好方法