拓扑排序
参考博客:https://blog.youkuaiyun.com/jasmine_shine/article/details/43488895;
什么是拓扑排序?
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。
该序列必须满足下面两个条件:
1.每个顶点出现且只出现一次。
2.若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序。
用自己的话说,就是把有向无环图中的点按照图中边的方向排序,在边前端的节点先出现,在边后端的节点后出现。
注意:拓扑排序的结果有时并不唯一
先说几个简单概念:
1.前驱(pre):一个点(V)如果与另外一个点(U)相连,且U在有向边(U,V)的前端,则称V存在前驱。
2.度(degree):一个节点的度指的是与该节点相关联的边的条数。对有向图来说,一个点的度可以分为入度和出度。
①入度:与该点相关联的边中以该点为终点的边的条数。
②出度:与该点相关联的边中以该点为起点的边的条数。
3.偏序:有向图中存在两个点,它们之间的关系是不明确的。即两个点不知谁在前谁在后。
4.全序:有向无环图中任意两个顶点的关系都是明确的(谁前谁后或者并排)。
拓扑排序的作用:
引用从学习过程中看到的一段话(出处没有保留,向该话博主致歉):
拓扑排序通常用来“排序”具有依赖关系的任务。
比如,如果用一个DAG图来表示一个工程,其中每个顶点表示工程中的一个任务,用有向边 (A,B)表示在做任务 B 之前必须先完成任务 A。故在这个工程中,任意两个任务要么具有确定的先后关系,要么是没有关系,绝对不存在互相矛盾的关系(即环路)。
也就是用来确定有向图中各个点的先后关系。
常用的方法如下(BFS思想实现):
从 DAG 图中选择一个没有前驱(即入度为0)的顶点并输出。
从图中删除该顶点和所有以它为起点的有向边。
重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
若存在环,则输出的节点小于总节点数。所以可以用一个计数器,记录“删掉”的环的总数,再与总节点数比较,看是否存在环。
代码:
以下代码为参考博客中的代码(BFS思想),其中添加了本人学习时的注释。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
#define MAX 9999
queue<int>myqueue;
int indegree[MAX];
struct node
{
int adjvex;//当前点
node* next; //往下的相邻的点
} adj[MAX];
int Create(node adj[],int n,int m)//邻接表建表函数,n代表定点数,m代表边数
{
int i;
node *p;
for(i=1; i<=n; i++)
{
adj[i].adjvex=i;
adj[i].next=NULL;
}
for(i=1; i<=m; i++)
{
cout<<"请输入第"<<i<<"条边:";
int u,v;
cin>>u>>v;
//下面链表的连接相当于用vector存储 u的相邻点
//相当于push_front()的操作
p=new node;
p->adjvex=v;
p->next=adj[u].next;
adj[u].next=p;
}
return 1;//创建成功
}
void print(int n)//邻接表打印函数
{
int i;
node *p;
for(i=1; i<=n; i++)
{
p=&adj[i];
while(p!=NULL)
{
cout<<p->adjvex<<' ';
p=p->next;
}
cout<<endl;
}
}
void topsort(node adj[],int n)
{
int i;
node *p;
memset(indegree,0,sizeof(indegree));
for(i=1; i<=n; i++)
{
//因为一个点只有作为另一个的后端时,该点才有入度一说,所以有如下操作
p=adj[i].next;
while(p!=NULL)
{
indegree[p->adjvex]++;//计算每一个点的入度
p=p->next;
}
}
for(i=1; i<=n; i++)
{
//寻找没有前驱的节点为起始节点
if(indegree[i]==0)
myqueue.push(i);
}
int count=0;//计数器,用于判断是否有环
//BFS思想
while(myqueue.size()!=0)
{
i=myqueue.front();
myqueue.pop();
//输出拓扑排序,因为只有没有前驱的点才可以入队,所以 队首元素肯定是符合条件的元素
cout<<i<<' ';
count++;
for(p=adj[i].next; p!=NULL; p=p->next)
{
int k=p->adjvex;
indegree[k]--;//相当于删掉两个点之间的边
//寻找没有前驱的点
if(indegree[k]==0)
myqueue.push(k);
}
}
cout<<endl;
if(count<n)cout<<"有回路"<<endl;
}
int main()
{
int n;
int m;
cout<<"请输入顶点数及边数:";
cin>>n>>m;
Create(adj,n,m);
cout<<"输入的邻接表为:"<<endl;
print(n);
cout<<"拓扑排序结果为:"<<endl;
topsort(adj,n);
system("pause");
return 0;
}
下面是学习后自己写的代码,也是基于BFS
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e3;
vector<int> storage;//存储排序后的节点
int indegree[MAXN]; //入度。 也可以声明为动态数组,比较灵活,不过访问时比较慢
void print()
{
for(int i=0;i<storage.size();++i)
cout<<storage.at(i)<<(i==storage.size()-1?'\n':' ');
}
void topological_sort(vector<int> v[],int n)
{
queue<int> q;
//寻找第一个点,即没有前驱的点
for(int i=1; i<=n; ++i)
if(!indegree[i])
q.push(i);
int cnt = 0; //计数器,判断是否存在回路
while(!q.empty())
{
int top = q.front();
q.pop();
cnt++;
storage.push_back(top);//所有符合条件入队的点都是排序好了的点
for(int i=0;i<v[top].size();++i)
{
int node = v[top][i];
indegree[node]--;
//需要下一个前驱为0的点
if(!indegree[node])
q.push(node);
}
}
if(cnt<n)
cout<<"该图存在回路!"<<endl;
else
print();
}
int main()
{
int n,m,u,v;
while(1)
{
cout<<"请输入节点数和边数:";
cin>>n>>m;
if(!n)
break;
vector<int> *V = new vector<int>[n+1] ; //用vector 存储边的对应关系
memset(indegree,0,sizeof(indegree));
storage.clear();
for(int i=1; i<=m; ++i)
{
cout<<"请输入第"<<i<<"条有向边:";
cin>>u>>v;
V[u].push_back(v);// 存储边 U->V
indegree[v]++;// 入度增加
}
topological_sort(V , n);
for(int i=0;i<=n;++i)
V[i].clear();
delete []V;
}
}
拓扑排序也可以是基于DFS的,但我暂时没学到。等学会后再补充。