0、总结
Get to the points firstly, the article comes from LawsonAbs!
- 基于队列实现拓扑排序;
- 如何判断拓扑排序不唯一?
- 如何对拓扑排序计数?
1、拓扑排序
前提:有向无环图(DAG)
1.1 什么是拓扑排序?
- 图形特征
一个DAG,除了起始点之外,每个点都有一个或多个前驱;除了终点之外,每个点都有一个或多个后继。 - 访问方法
仅当一个节点的所有前驱全部访问完毕时,才可以访问改节点。若同时有多个节点可以访问,随机选择一个即可。
基于上述的访问方法,给出访问得到的序列,这个序列就是拓扑排序。
2、 基于队列实现
2.1 主要思想
- 用gra[][]数组保存节点间是否有边 【可以用
bool数组来存储】 - 使用优先队列。队列中的基本元素是
Node。Node包含两个元素:节点序号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. 其它
拓扑排序相关题还有:

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

被折叠的 条评论
为什么被折叠?



