拓扑排序及两种实现(BFS,DFS)

定义

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

由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序(topological-sort,别称 topsort),由于图的邻接表更合适稀疏图,因此下面的算法实现都是邻接表存储图

举例

以我们平时起床的穿衣顺序为例。如下图1,秋裤,裤子,腰带的顺序是一定的;袜子和鞋子的顺序是一定的;衬衫,领带,夹克的顺序是一定的;至于手表,我们随时都可以戴,因此并不影响我们穿其他的衣服。在保证每一块的顺序一定的情况下,我们也可以穿秋裤之后穿袜子,然后穿衬衫,但是裤子肯定不能排在秋裤前面,鞋子也肯定不能排在裤子或者袜子前面(如果有奇葩当我没说)。图二是其中一个拓扑序列,我们平时的个人穿衣顺序都是拓扑序列

在这里插入图片描述

我们可以观察到,拓扑序列并不唯一,但是拓扑序列的起点肯定是入度为0的点

BFS拓扑排序

实现思路1:无前驱节点优先

  1. 在有向图中选一个无前驱的顶点并且输出
  2. 从图中删除所有和它有关的边
  3. 重复上述两步,直至所有顶点输出。如果还有顶点未输出,则说明有环
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;
}

我们还可以发现,如果把恢复操作省略,那么得到的就是一个拓扑序列,因此该方法也作为求任意拓扑序列的一个较好方法

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值