图的拓扑排序

本文链接:http://www.cnblogs.com/Ash-ly/p/5398310.html

定义:

  图的拓扑排序是对有向无环图来说的,无向图和有环图没有拓扑排序,或者说不存在拓扑排序,对于一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若图G存在边< u, v >,则u在线性序列中出现在v之前。对一个有向无环图进行拓扑排序产生的线性序列称为满足拓扑次序的序列。一个有向无环图可以表示某种动作或者方案,或者状态,而有向无环图的拓扑序列通常表示某种某案切实可行或者各个成员之间的某种关系.

  举个栗子,看上面那个图,v1v2v3v4v5v6v7v8是有向图的一个拓扑序列,同时v1v3v5v2v4v6v7v8也是有向图的一个拓扑序列,即对于一个都有向图来说拓扑序列不止一个,在拓扑序列中所有有向边的起点都在终点的左边,也就是如若有向图G中从顶点vi到vj有一条路径,则在拓扑序列中顶点vi必须在顶点vj之前.

算法:

  (1):在有向图中选一个没有前驱(入度为0)的顶点输出之。

  (2):从图中删除该顶点和所有以它为尾的弧(从该点出发的所有有向边)。

  (3):重复上述步骤,直至全部顶点均已输出,或者当前图中不存在没有前驱的顶点为止.后一种情况则说明图中有环。

  实现算法事采用链式前向星存储图,再开一个额外的数组存储每个点的入度,每删除一个点,就遍历以这个点为起点的边,将边对应的终点的入度减1,即可选择并删除下一点。在如何选择下一个点的问题上,需要开一个栈,当出现入度为0的顶点是就把其压入栈,然后每次从栈顶获得一个元素,一定是下一个待扩展元素。

代码:

 1 #include <iostream>
 2 #include <cmath>
 3 #include <cstdio>
 4 #include <cstring>
 5 #include <cstdlib>
 6 #include <algorithm>
 7 #include <stack>
 8 using namespace std;
 9 typedef long long LL;
10 
11 const int maxN = 100 + 3;
12 const int maxM = 1000 + 3;
13 int head[maxN];
14 int N, M;
15 
16 struct EdgeNode
17 {
18     int to;
19     int w;
20     int next;
21 };
22 EdgeNode Edges[maxM];
23 
24 
25 int indegree[maxN];//用于记录每个点的入度
26 
27 //获得每个点的入度
28 int getIdg()
29 {
30     memset(indegree, 0, sizeof(indegree));
31     for(int i = 1; i <= M; i++)
32         indegree[ Edges[i].to ]++;
33 }
34 void tplgSort()
35 {
36     getIdg();
37     stack<int> st;
38     for(int i = 1; i <= N; i++) //把入度为 0 的顶点压入栈中
39         if( !indegree[i] ) st.push(i);
40     while( !st.empty() )
41     {
42         int tp = st.top();  //每次从栈顶获得一个顶点,一定是下一个待扩展顶点
43         cout <<  tp << " ";
44         st.pop();//弹出栈顶元素
45         //删除从该节点出发的全部有向边,更新 indegree 数组
46         for(int i = head[ tp ]; i != -1; i = Edges[i].next )
47         {
48             --indegree[ Edges[i].to ];//对于从 i 号节点的每个邻接节点入度 -1
49             if( !indegree[ Edges[i].to ] ) //如果入度为 0 则入栈
50                 st.push( Edges[i].to );
51         }
52     }
53 }
54 
55 int main()
56 {
57     //freopen("input.txt", "r", stdin);
58     memset(head, -1, sizeof(head));
59     cin >> N >> M;
60     for(int i = 1; i <= M; i++)
61     {
62         int x, y ,z;
63         cin >> x >> y >> z;
64         Edges[i].to = y;
65         Edges[i].w = z;
66         Edges[i].next = head[x];
67         head[x] = i;
68     }
69     tplgSort();
70     return 0;
71 }

  对于上述算法,可在每次弹出元素时设置一个计数变量,如果这个计数变量最后小于顶点的个数,说明该有向图有回路,即不存在拓扑序列。该算法在O(m)的时间内对indegree数组进行初始化,在O(n)的数组里对st进行初始化,之后虽然是两次循环,但是每条边只遍历了一次,所以时间复杂度应该是O(m).

拓展:

  对于某些时候题目要求输出找到所有的拓扑序列,这时候上面这种只能找到一个拓扑序列的算法就不行了,找到所有拓扑序列的算法任然适用每一步总时输出当前无前驱的顶点这一性质,使用深度优先搜索方法来解决。

伪代码:

 1 //参数G为图,L为已经排序的顶点
 2 TplgSort(Graph G, List L) {
 3     G中所有没有前驱的顶点(入度为0)放入队列Q(局部);
 4     while(队列Q不为空) {
 5         Q中顶点出队,设为V;
 6         复制L生成L的副本CPL,并把V加入CPL;
 7         复制G生成G的副本CPG,从CPG中删去V和以V为起点的有向边;
 8         if (CPG中没有顶点) {
 9             输出CPL; //CPL为一种拓扑序列
10         } else {
11             TplgSort(CPG, CPL);//对于剩下的图进行递归求解
12         }
13     }
14 }//算法有点类似回溯,在解空间中利用DFS求所满足的拓扑序列

代码:

  具体实现时,利用head[i] = -1来删除从这个点出发的所有有向边,利用vis[i] = 1来表示这个点已经被访问(删除),利用队列存储答案。

 1 const int maxN = 100 + 3;
 2 const int maxM = 1000 + 3;
 3 int head[maxN];
 4 int N, M;
 5 
 6 struct EdgeNode
 7 {
 8     int to;
 9     int w;
10     int next;
11 };
12 EdgeNode Edges[maxM];
13 
14 int indegree[maxN];
15 
16 void getIdg(int vis[])//获得每个点的入度,其实可以再输入数据时就统计的,这里写成了一个函数,方便看
17 {
18     memset(indegree, 0, sizeof(indegree));
19     for(int i = 1; i <= N; i++)
20     {
21         if(vis[i] == 0)
22         {
23              for(int j = head[i]; j!= -1; j = Edges[j].next)
24              {
25                  indegree[Edges[j].to]++;
26              }
27         }
28     }
29 
30 }
31 
32 int vis[maxN + 7] = {0};
33 int allVis()      //此图所有点是否已经被全部遍历
34 {
35     for(int i = 1;i <= N; i++)
36         if( !vis[i] ) return 0;
37     return 1;
38 }
39 
40 void DFS(int head[], int vis[], queue<int> Qu)
41 {
42     queue <int> q;//局部队列q
43     getIdg(vis);
44     for(int i = 1; i <= N; i++) //每次把入度为零的加入到新的队列中
45         if( !vis[i] && !indegree[i] ) q.push(i);
46     while( !q.empty())
47     {
48         int v = q.front();
49         q.pop();
50         queue <int> cpyQu(Qu); //创建Qu的副本并把点 v 加入其中
51         cpyQu.push(v);
52         vis[v] = 1;        //删除点
53         int t = head[v];
54         head[v] = -1;        //删除从该点出发的所有有向边
55         if(allVis())    //如果图中的点全部被访问,即图中没有剩余的点
56         {
57             while(!cpyQu.empty())//输出答案
58             {
59                 cout << cpyQu.front() << " ";
60                 cpyQu.pop();
61             }
62             cout << endl;
63         }
64         else
65             DFS(head, vis, cpyQu);
66         vis[v] = 0;  //回溯时恢复现场
67         head[v] = t;
68     }
69 }
70 
71 int main()
72 {
73     //freopen("input.txt", "r", stdin);
74     memset(head, -1, sizeof(head));
75     cin >> N >> M;
76     for(int i = 1; i <= M; i++)
77     {
78         int x, y ,z;
79         cin >> x >> y >> z;
80         Edges[i].to = y;
81         Edges[i].w = z;
82         Edges[i].next = head[x];
83         head[x] = i;
84     }
85     memset(vis, 0, sizeof(vis));
86     queue <int> Qu;
87     DFS(head, vis, Qu);
88     return 0;
89 }

  在某些时候,题目中又要求我们输出逆序的拓扑序列。在这个时候可以想象利用DFS进行遍历一个拓扑排序,因为图中无环,则由图中某点出发进行DFS时,最先退出DFS函数的顶点即为出度为零的顶点,是拓扑序列中最后一个顶点。由此按照退出DFS函数的先后记录下来的顶点序列即为逆序的拓扑序列。其实还有个比较有意思的,就是对于一个有向图,把所有边的方向都反过来,再求拓扑序列,其实也是原图的逆向拓扑序列。这个思想可灵活运用于按照字典序输出所有的拓扑序列,按照字典序输出所有的逆向拓扑序列。

代码:

 1 //逆向的拓扑有序
 2 int visited[maxN] = {0};
 3 void DFS(int x)
 4 {
 5     visited[x] = 1; //标记当前节点已被访问
 6     for(int i = head[x]; i != -1; i = Edges[i].next)
 7     {
 8     //对于每个未被访问的相邻节点,调用DFS,遍历结束后、尝试其他支路
 9         if( !visited[Edges[i].to] )
10             DFS(Edges[i].to);
11     }
12     cout << x <<" ";//逆序的拓扑序列
13 }

 

转载于:https://www.cnblogs.com/Ash-ly/p/5398310.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值