文字部分参考来源:http://blog.csdn.net/qq_35644234/article/details/60578189
1、拓扑排序的介绍
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。
拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
2、拓扑排序的实现步骤
- 在有向图中选一个没有前驱的顶点并且输出
- 从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)
- 重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。
3、拓扑排序示例手动实现
如果我们有如下的一个有向无环图,我们需要对这个图的顶点进行拓扑排序,过程如下:
首先,我们发现V6和v1是没有前驱的,所以我们就随机选去一个输出,我们先输出V6,删除和V6有关的边,得到如下图结果:
然后,我们继续寻找没有前驱的顶点,发现V1没有前驱,所以输出V1,删除和V1有关的边,得到下图的结果:
然后,我们又发现V4和V3都是没有前驱的,那么我们就随机选取一个顶点输出(具体看你实现的算法和图存储结构),我们输出V4,得到如下图结果:
然后,我们输出没有前驱的顶点V3,得到如下结果:
然后,我们分别输出V5和V2,最后全部顶点输出完成,该图的一个拓扑序列为:
v6–>v1—->v4—>v3—>v5—>v2
4、拓扑排序的代码实现
这里采用链式前向星结构存储图,利用无前驱节点优先扩展的拓扑排序算法。
样例图:
数据输入:
先输入节点数n和边的数目m,然后输入每条边的信息i、j、w表示节点i到节点j之间有边,权值w。上图所有边的权值都为1.
样例图数据:
10 12
0 2 1
1 2 1
1 3 1
2 4 1
3 5 1
3 6 1
7 4 1
4 5 1
5 8 1
5 9 1
6 9 1
9 8 1
代码如下:(使用链式前向星结构存储图)
1 #include <stdio.h> 2 3 #define maxN 1000 4 #define maxM 2000 5 6 int head[maxN]; 7 struct EdgeNode 8 { 9 int to; 10 int w; 11 int next; 12 }; 13 struct EdgeNode Edges[maxM]; 14 15 int n,m;//n个点m条边的图 16 int indegree[maxN]; 17 18 void topo_sort();//利用队列完成无前驱节点优先的拓扑排序 19 20 int main(int argc, char *argv[]) 21 { 22 int i,j,w,k; 23 freopen("data2.in","r",stdin); 24 //freopen("data.out","w",stdout); 25 26 scanf("%d%d",&n,&m); 27 for(i=0;i<n;i++) { head[i]=-1; indegree[i]=0; } 28 for(k=0;k<m;k++) 29 { 30 scanf("%d%d%d",&i,&j,&w); 31 Edges[k].to=j; 32 Edges[k].w=w; 33 Edges[k].next=head[i]; 34 head[i]=k;//head[i]存储以点vi出发的第一条边在Edges[]中的位置。 35 36 indegree[j]++;//统计各个节点的入度 37 } 38 39 topo_sort(); 40 41 return 0; 42 } 43 void topo_sort()//利用队列完成无前驱节点优先的拓扑排序 44 { 45 int queueArr[maxN]; 46 int iq=0;//队列的队尾下标 47 int i,k; 48 49 //将无前驱节点入队 50 for(i=0;i<n;i++) 51 { 52 if(indegree[i]==0) queueArr[iq++]=i; 53 } 54 55 //选择队头节点作为新的拓扑节点并更新indegree[],生成拓扑排序序列 56 for(i=0;i<iq;i++) 57 { 58 //删除该顶点出发的全部有向边,更新indegree[] 59 for(k=head[queueArr[i]];k!=-1;k=Edges[k].next) 60 { 61 indegree[Edges[k].to]--; 62 if(indegree[Edges[k].to]==0)//若Edges[k].to的入度为0则该点已经没有前驱节点,可以入队 63 queueArr[iq++]=Edges[k].to; 64 } 65 } 66 //输出拓扑排序序列 67 for(i=0;i<iq;i++) printf("%d ",queueArr[i]); 68 }
上述算法的时间复杂度为O(n+m),n为节点数,m为边的数目。
运行结果:
下面这一段代码是利用前向星结构的拓扑排序,原理和上面的代码基本一致:
1 #include<stdio.h> 2 #include<iostream> 3 #include<stdlib.h> 4 #include<string.h> 5 #include<algorithm> 6 using namespace std; 7 8 #define maxN 1000 9 #define maxM 2000 10 11 struct NODE 12 { 13 int from; //边的起点 14 int to; //边的终点 15 }; 16 struct NODE edge[maxM]; //边数组 17 int head[maxN]; //存储出发点为 Vi 的第一条边在 edge[ ]中的位置,一般初始化为-1。 18 19 int n,m;//n个点m条边的图 20 int indegree[maxN]; 21 22 bool cmp(NODE a,NODE b) 23 { 24 if(a.from==b.from)return a.to<b.to; 25 return a.from<b.from; 26 } 27 void topo_sort();//利用队列完成无前驱节点优先的拓扑排序 28 29 int main(int argc, char *argv[]) 30 { 31 int i; 32 freopen("data.in","r",stdin); 33 scanf("%d%d",&n,&m); 34 for(i=0;i<m;i++) cin>>edge[i].from>>edge[i].to; 35 sort(edge,edge+m,cmp); 36 //for(i=0;i<m;i++) printf("%d %d\n",edge[i].from,edge[i].to);//测试代码 37 memset(head,-1,sizeof(head)); 38 head[edge[0].from]=0; 39 indegree[edge[0].to]=1; 40 for(i=1;i<m;i++) 41 { 42 if(edge[i].from != edge[i-1].from) 43 { 44 head[edge[i].from]=i;//标记以第 i 个点做起点的第一条边在 edge[]的位置 45 } 46 indegree[edge[i].to]++;//记录各个顶点的入度 47 } 48 //for(i=1;i<=n;i++) printf("%d ",indegree[i]); printf("\n"); //测试代码:输出各个点的入度.(题目数据顶点编号从1开始) 49 topo_sort(); 50 return 0; 51 } 52 void topo_sort()//利用队列完成无前驱节点优先的拓扑排序. 53 { 54 int queueArr[maxN]; 55 int iq=0;//队列的队尾下标 56 int i,k; 57 58 //将无前驱节点入队 59 for(i=1;i<=n;i++) 60 { 61 if(indegree[i]==0) queueArr[iq++]=i; 62 } 63 //测试代码for(i=0;i<iq;i++) printf("%d ",queueArr[i]);printf("\n"); 64 65 //选择队头节点作为新的拓扑节点并更新indegree[],生成拓扑排序序列 66 for(i=0;i<iq;i++) 67 { 68 if(head[queueArr[i]]!=-1)//该顶点有邻接点 69 { 70 //遍历该顶点出发的全部有向边 71 for(k=head[queueArr[i]];edge[k].from==queueArr[i]&&k<m;k++) 72 { 73 indegree[edge[k].to]--; 74 if(indegree[edge[k].to]==0)//若Edges[k].to的入度为0则该点已经没有前驱节点,可以入队 75 queueArr[iq++]=edge[k].to; 76 } 77 head[queueArr[i]]=-1;//删除该顶点出发的全部有向边 78 } 79 } 80 //输出拓扑排序序列 81 for(i=0;i<iq;i++) printf("v%d ",queueArr[i]); 82 printf("\n"); 83 }
=========分割线开始================================================================
下面是一道关于拓扑排序的OJ题目
4084:拓扑排序
-
总时间限制: 1000ms 内存限制: 65536kB
-
描述
-
给出一个图的结构,输出其拓扑排序序列,要求在同等条件下,编号小的顶点在前。
输入
-
若干行整数,第一行有2个数,分别为顶点数v和弧数a,接下来有a行,每一行有2个数,分别是该条弧所关联的两个顶点编号。
v<=100, a<=500
输出
- 若干个空格隔开的顶点构成的序列(用小写字母)。 样例输入
-
6 8 1 2 1 3 1 4 3 2 3 5 4 5 6 4 6 5
样例输出
-
v1 v3 v2 v6 v4 v5
利用前向星结构或者链式前向星结构或者其他结构都可以,这些结构唯一不同在于寻找邻接节点的方式稍有不同。下面是前向星结构。
为了能够实现题目要求的“在同等条件下,编号小的顶点在前”这个条件,数据规模不大的情况下,非常粗暴地使用了n^2级别的算法。详细的请看代码:
1 /*==================================================================== 2 4084:拓扑排序 3 总时间限制: 1000ms 内存限制: 65536kB 4 描述 5 给出一个图的结构,输出其拓扑排序序列,要求在同等条件下,编号小的顶点在前。 6 7 输入 8 若干行整数,第一行有2个数,分别为顶点数v和弧数a,接下来有a行,每一行有2个数, 9 分别是该条弧所关联的两个顶点编号。 10 v<=100, a<=500 11 输出 12 若干个空格隔开的顶点构成的序列(用小写字母)。 13 样例输入 14 6 8 15 1 2 16 1 3 17 1 4 18 3 2 19 3 5 20 4 5 21 6 4 22 6 5 23 样例输出 24 v1 v3 v2 v6 v4 v5 25 ======================================================================*/ 26 #include<stdio.h> 27 #include<iostream> 28 #include<stdlib.h> 29 #include<string.h> 30 #include<algorithm> 31 using namespace std; 32 33 #define maxN 1000 34 #define maxM 2000 35 36 struct NODE 37 { 38 int from; //边的起点 39 int to; //边的终点 40 }; 41 struct NODE edge[maxM]; //边数组 42 int head[maxN]; //存储出发点为 Vi 的第一条边在 edge[ ]中的位置,一般初始化为-1。 43 44 int n,m;//n个点m条边的图 45 int indegree[maxN]; 46 int ttt; 47 48 bool cmp(NODE a,NODE b) 49 { 50 if(a.from==b.from)return a.to<b.to; 51 return a.from<b.from; 52 } 53 void topo_sort();//利用队列完成无前驱节点优先的拓扑排序. 编号小的节点优先输出. 54 int main(int argc, char *argv[]) 55 { 56 int i; 57 freopen("data.in","r",stdin); 58 59 scanf("%d%d",&n,&m); 60 for(i=0;i<m;i++) cin>>edge[i].from>>edge[i].to; 61 sort(edge,edge+m,cmp); 62 //for(i=0;i<m;i++) printf("%d %d\n",edge[i].from,edge[i].to);//测试代码 63 memset(head,-1,sizeof(head)); 64 head[edge[0].from]=0; 65 indegree[edge[0].to]=1; 66 for(i=1;i<m;i++) 67 { 68 if(edge[i].from != edge[i-1].from) 69 { 70 head[edge[i].from]=i;//标记以第 i 个点做起点的第一条边在 edge[]的位置 71 } 72 indegree[edge[i].to]++;//记录各个顶点的入度 73 } 74 //for(i=1;i<=n;i++) printf("%d ",indegree[i]); printf("\n"); //测试代码:输出各个点的入度.(题目数据顶点编号从1开始) 75 76 topo_sort(); 77 78 return 0; 79 } 80 81 void topo_sort()//利用队列完成无前驱节点优先的拓扑排序. 编号小的节点优先输出. 82 { 83 int i,k; 84 ttt=n; 85 while(ttt>0) 86 { 87 for(i=1;i<=n;i++)//扫描寻找编号最小的无前驱节点 88 { 89 if(indegree[i]==0) 90 { 91 printf("v%d ",i); 92 ttt--; 93 if(head[i]!=-1)//该顶点有邻接点 94 { 95 //遍历该顶点出发的全部有向边,把这些边的终点的入度减1. 96 for(k=head[i];edge[k].from==i&&k<m;k++) 97 { 98 indegree[edge[k].to]--; 99 } 100 head[i]=-1;//删除该顶点出发的全部有向边 101 } 102 indegree[i]=-1; 103 break; 104 } 105 } 106 } 107 }
这道题假如使用优先队列解决应该会好一些吧。
===========分割线结束=======================================================
原文有讲到两种算法:
- Kahn算法
- 基于DFS的拓扑排序算法
首先我们先介绍第一个算法的思路:
Kahn的算法的思路其实就是我们之前那个手动展示的拓扑排序的实现,我们先使用一个栈保存入度为0 的顶点,然后输出栈顶元素并且将和栈顶元素有关的边删除,减少和栈顶元素有关的顶点的入度数量并且把入度减少到0的顶点也入栈。具体的代码如下:
1 bool Graph_DG::topological_sort() { 2 cout << "图的拓扑序列为:" << endl; 3 //栈s用于保存栈为空的顶点下标 4 stack<int> s; 5 int i; 6 ArcNode * temp; 7 //计算每个顶点的入度,保存在indgree数组中 8 for (i = 0; i != this->vexnum; i++) { 9 temp = this->arc[i].firstarc; 10 while (temp) { 11 ++this->indegree[temp->adjvex]; 12 temp = temp->next; 13 } 14 15 } 16 17 //把入度为0的顶点入栈 18 for (i = 0; i != this->vexnum; i++) { 19 if (!indegree[i]) { 20 s.push(i); 21 } 22 } 23 //count用于计算输出的顶点个数 24 int count=0; 25 while (!s.empty()) {//如果栈为空,则结束循环 26 i = s.top(); 27 s.pop();//保存栈顶元素,并且栈顶元素出栈 28 cout << this->arc[i].data<<" ";//输出拓扑序列 29 temp = this->arc[i].firstarc; 30 while (temp) { 31 if (!(--this->indegree[temp->adjvex])) {//如果入度减少到为0,则入栈 32 s.push(temp->adjvex); 33 } 34 temp = temp->next; 35 } 36 ++count; 37 } 38 if (count == this->vexnum) { 39 cout << endl; 40 return true; 41 } 42 cout << "此图有环,无拓扑序列" << endl; 43 return false;//说明这个图有环 44 }
第二个算法的思路:
其实DFS就是深度优先搜索,它每次都沿着一条路径一直往下搜索,知道某个顶点没有了出度时,就停止递归,往回走,所以我们就用DFS的这个思路,我们可以得到一个有向无环图的拓扑序列,其实DFS很像Kahn算法的逆过程。具体的代码实现如下:
1 bool Graph_DG::topological_sort_by_dfs() { 2 stack<string> result; 3 int i; 4 bool * visit = new bool[this->vexnum]; 5 //初始化我们的visit数组 6 memset(visit, 0, this->vexnum); 7 cout << "基于DFS的拓扑排序为:" << endl; 8 //开始执行DFS算法 9 for (i = 0; i < this->vexnum; i++) { 10 if (!visit[i]) { 11 dfs(i, visit, result); 12 } 13 } 14 //输出拓扑序列,因为我们每次都是找到了出度为0的顶点加入栈中, 15 //所以输出时其实就要逆序输出,这样就是每次都是输出入度为0的顶点 16 for (i = 0; i < this->vexnum; i++) { 17 cout << result.top() << " "; 18 result.pop(); 19 } 20 cout << endl; 21 return true; 22 } 23 void Graph_DG::dfs(int n, bool * & visit, stack<string> & result) { 24 25 visit[n] = true; 26 ArcNode * temp = this->arc[n].firstarc; 27 while (temp) { 28 if (!visit[temp->adjvex]) { 29 dfs(temp->adjvex, visit,result); 30 } 31 temp = temp->next; 32 } 33 //由于加入顶点到集合中的时机是在dfs方法即将退出之时, 34 //而dfs方法本身是个递归方法, 35 //仅仅要当前顶点还存在边指向其他不论什么顶点, 36 //它就会递归调用dfs方法,而不会退出。 37 //因此,退出dfs方法,意味着当前顶点没有指向其他顶点的边了 38 //,即当前顶点是一条路径上的最后一个顶点。 39 //换句话说其实就是此时该顶点出度为0了 40 result.push(this->arc[n].data); 41 42 }
两种算法总结:
对于基于DFS的算法,增加结果集的条件是:顶点的出度为0。这个条件和Kahn算法中入度为0的顶点集合似乎有着异曲同工之妙,Kahn算法不须要检测图是否为DAG,假设图为DAG,那么在入度为0的栈为空之后,图中还存在没有被移除的边,这就说明了图中存在环路。而基于DFS的算法须要首先确定图为DAG,当然也可以做出适当调整,让环路的检测測和拓扑排序同一时候进行,毕竟环路检測也可以在DFS的基础上进行。
二者的复杂度均为O(V+E)。