【算法竞赛刷题模板9】拓扑排序

本文深入探讨了基于队列实现的拓扑排序算法,包括其原理、实现代码及如何判断拓扑排序的唯一性。通过具体实例,展示了如何利用拓扑排序解决实际问题,并提供了洛谷P1960题目的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


0、总结

Get to the points firstly, the article comes from LawsonAbs!

  • 基于队列实现拓扑排序;
  • 如何判断拓扑排序不唯一?
  • 如何对拓扑排序计数?

1、拓扑排序

前提:有向无环图(DAG

1.1 什么是拓扑排序?

  • 图形特征
    一个DAG,除了起始点之外,每个点都有一个或多个前驱;除了终点之外,每个点都有一个或多个后继。
  • 访问方法
    仅当一个节点的所有前驱全部访问完毕时,才可以访问改节点。若同时有多个节点可以访问,随机选择一个即可。
    基于上述的访问方法,给出访问得到的序列,这个序列就是拓扑排序。

2、 基于队列实现

2.1 主要思想

  • 用gra[][]数组保存节点间是否有边 【可以用 bool 数组来存储】
  • 使用优先队列。队列中的基本元素是 NodeNode包含两个元素:节点序号id,节点的入度 dig
  • 优先队列可以保证每次出队的都是入度为0的节点,根据出队的元素消除此节点带来的for循环
  • 因为是不停的把入度为0的元素放到队列中,所以队列中的节点可能会存在重复。故使用vis[]来避免重复访问

2.2 代码

#include<iostream> 
#include<queue>
using namespace std;
const int maxN = 5005;
bool gra[maxN][maxN];//gra[i][j]=1表示 i->j 
int vis[maxN],in[maxN];//是否已经访问;表示顶点的入度信息

// typedef 的作用就是为一种数据类型定义一个新名字。用在这里就是给Node{...} 这个类型定义为Node
typedef struct Node{
	int id,dig;	
	bool operator<(const Node& n) const{//自定义排序规则
		return n.dig < this->dig;
	}
}Node;
//存放一个顶点的下标及其入度 
priority_queue<Node> pq; 

int main(){
	int n,m;
	cin >> n >> m;	
	int a,b; 
	fill(in,in+maxN,0);
	int flag = 0; 
	for(int i = 0;i< m;i++){
		cin >> a>>b;
		gra[a][b] = 1;//a 可以打败 b 
		in[b]++;
	}
	for(int i = 1;i<= n;i++){
		if(in[i] == 0){
			pq.push((Node){i,0});	
			break;
		}
	}
	
	int cur,tot = 0;
	Node temp;
	int ans[maxN];//拓扑排序序列
	// 队列非空时
	while(!pq.empty()){	
		temp = pq.top();
		cur = temp.id;
		if(vis[cur] == 0){//防止重复访问 
			vis[cur] = 1;
			ans[tot++] = cur;	
		} 
		for(int i = 1; i<=n; i++){
			if(gra[cur][i]){//减少入度
				in[i]--;
			}
			if(vis[i] == 0 &&in[i] == 0){//说明没有入度了 					
				pq.push((Node){i,0});
			}
		}
		pq.pop();
		temp = pq.top();
		cur = temp.id;				
	}
	for(int i = 0;i< tot;i++){
		cout << ans[i] <<"\n"; 
	}	
}

测试用例

3
3
2 1
2 3
3 1
Output: 2 3 1

3
2
2  1
2  3
Output: 2 1 3

3、思考

3.1 拓扑排序唯一吗?

显然不唯一的,为什么不唯一呢?在代码中又如何判别呢?
拓扑排序不唯一的原因是:在进行拓扑排序的时候,有多个可以选择的 入度为0的节点,但是由于每次只能选择一个进行排序,故可生成多个排序序列。如上例中的:

3
2
2  1
2  3

其拓扑序列既可以是2 1 3,又可以是2 3 1
仔细思考一下,上面叙述的原因并非拓扑排序不唯一的根本原因,那么其根本原因是什么?其根本原因就是在某个节点访问完之后,同时造成多个节点的入度为0。 也就是说,我们在代码

if(vis[i] == 0 &&in[i] == 0){//说明没有入度了 					
	pq.push((Node){i,0});			
}

中进行判断是否同时加入了2个或以上的节点,就可以知道该 DAG 的拓扑排序是否唯一了。

完整代码如下:

#include<iostream> 
#include<queue>
using namespace std;
const int maxN = 5005;
bool gra[maxN][maxN];//gra[i][j]=1表示 i可以打败j 
int vis[maxN],in[maxN];//是否已经访问;表示顶点的入度信息

typedef struct Node{
	int id,dig;	
	bool operator<(const Node& n) const{
		return n.dig < this->dig;
	}
}Node;
//存放一个顶点的下标及其入度 
priority_queue<Node> pq; 
queue<Node> que;

int main(){
	int n,m;
	cin >> n >> m;	
	int a,b; 
	fill(in,in+maxN,0);	
	for(int i = 0;i< m;i++){
		cin >> a>>b;
		gra[a][b] = 1;//a 可以打败 b 
		in[b]++;
	}
	for(int i = 1;i<= n;i++){
		if(in[i] == 0){
			pq.push((Node){i,0});	
			break;
		}
	}	
	int cur,tot = 0,cnt = 0,flag = 0;//cnt用于判断拓扑排序是否唯一
	Node temp;
	int ans[maxN];//最后的结果输出 
	// 队列非空时
	//前提:有向无环图 
	while(!pq.empty()){	
		cnt = 0;//重置
		temp = pq.top();
		cur = temp.id;
		if(vis[cur]){
			pq.pop();
			continue;			
		}
		
		vis[cur] = 1;
		ans[tot++] = cur;		
		for(int i = 1; i<=n; i++){
			if(gra[cur][i]){//减少入度
				in[i]--;
			}
			if(vis[i] == 0 &&in[i] == 0){//说明没有入度了 					
				pq.push((Node){i,0});
				cnt++;
			}
		}
		if(cnt > 1){
			flag = 1;
		}
		pq.pop();
		temp = pq.top();
		cur = temp.id;				
	}
	for(int i = 0;i< tot;i++){
		cout << ans[i] <<" "; 
	}
	if(flag)
		cout << 1<<"\n"; 
	else 
		cout <<0<<"\n";
}

测试用例

3
3
2 1
2 3
3 1
2 3 1 
0【0代表拓扑排序唯一】


3
2
2  1
2  3
2 1 3
1【1代表拓扑排序不唯一】

将上述代码的输出稍作修改,就可以应用到洛谷题目 洛谷 P1960 郁闷的记者 中。

3.2 如何对拓扑排序计数?

上面谈到拓扑排序可能不唯一,那么如何对拓扑排序进行计数呢?主要有两种方法,分别是:记忆化搜索和递推。

3.2.1 记忆化搜索

我们先谈记忆化搜索,这是深搜的优化版,朴素深搜在处理小数据量的问题时,是绝对可以的,但是如果数据量大了,则需要使用记忆化搜索来减少冗余的计算。

  • 如果一个节点到终点的数据已经计算过,那么就可以直接返回,而不用再深搜了;
  • 如果该节点尚未计算过,那么就要继续深搜直到遇到终点。

3.2.2 递推

  • 需要用到入度作为一个基础的数组支撑
  • 需要使用队列
  • 有点儿dp的味道

4. 其它

拓扑排序相关题还有:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说文科技

看书人不妨赏个酒钱?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值