链式前向星结构的图的拓扑排序算法 【采用无前驱节点优先的算法策略】

文字部分参考来源: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、拓扑排序的实现步骤

  1. 在有向图中选一个没有前驱的顶点并且输出
  2. 从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)
  3. 重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。

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 }
View Code

 

=========分割线开始================================================================

下面是一道关于拓扑排序的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 }
View Code

这道题假如使用优先队列解决应该会好一些吧。

===========分割线结束=======================================================

原文有讲到两种算法:

  1. Kahn算法
  2. 基于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 }
View Code

第二个算法的思路: 
其实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 }
View Code

两种算法总结: 
对于基于DFS的算法,增加结果集的条件是:顶点的出度为0。这个条件和Kahn算法中入度为0的顶点集合似乎有着异曲同工之妙,Kahn算法不须要检测图是否为DAG,假设图为DAG,那么在入度为0的栈为空之后,图中还存在没有被移除的边,这就说明了图中存在环路。而基于DFS的算法须要首先确定图为DAG,当然也可以做出适当调整,让环路的检测測和拓扑排序同一时候进行,毕竟环路检測也可以在DFS的基础上进行。 
二者的复杂度均为O(V+E)。

 

转载于:https://www.cnblogs.com/huashanqingzhu/p/8622754.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值