拓扑排序
背景
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的,但有些子工程没有先决条件,可以安排在任何时间开始。为了形象地反映出整个工程中各个子工程(活动)之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
问题描述
假定一个计算机专业的学生必须完成如图3-4所列出的全部课程。在这里,课程代表活动,学习一门课程就表示进行一项活动,学习每门课程的先决条件是学完它的全部先修课程。如学习《数据结构》课程就必须安排在学完它的两门先修课程《离散数学》和《算法语言》之后。学习《高等数学》课程则可以随时安排,因为它是基础课程,没有先修课。问题:求出一种可行的课程安排顺序。
基本要求
(1) 输入n(课程数量),m(限制条件个数,即一共所需的先修课程数量);
(2) 输入m行限制条件,每行两个数字a b,分别表示学习课程 前者的先修课程;
(3) 输出一个合法的课程排序方案。
测试数据
输入
9 11
3 1
3 2
4 3
4 5
5 2
6 4
6 5
7 4
7 9
8 1
9 8
输出
1 2 3 5 4 6 8 9 7
算法思想
定义一个二维数组,输入每个限制条件时,即表示所学课程(第一个数字)的入度加一,
输入完毕之后,执行程序。
(1) 查询第一个入度为0的课程序号,说明此课程没有先修课程,可以直接开始学习;
(2) 记录课程序号,把入度标记为-1;
(3) 遍历所有课程,把前驱含有上述课程序号的课程入度减一;
(4) 循环完毕,输出记录。
实现过程
|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
后驱 |
3,8 |
3,5 |
4, |
6,7 |
4,6 |
|
|
9 |
7, |
前驱 |
|
|
1,2, |
3,5 |
2, |
4,5, |
4,9, |
1, |
8, |
前驱为0的序号为1,记录A[i++]=1,标记为-1;然后把1的后驱的课程序号的入度减一。得到下表。
|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
后驱 |
|
3.,5 |
4 |
6,7 |
4,6 |
|
|
9 |
7 |
前驱 |
-1 |
|
2 |
3,5 |
2 |
4,5 |
4,9 |
|
8 |
前驱为0的序号为2,记录A[i++]=2,标记为-1;然后把2的后驱的课程序号的入度减一。
重复此过程,直到没有入度(前驱)为0的点,得到的A[]序列即为所求课程合法安排。
当A[]中的点的个数少于总数时,说明存在环,不能形成拓扑排序。
代码实现
一:二维数组
#include<stdio.h> #include<iostream> using namespace std; int map[510][510]={0};//存储有关联的点 int indegree[510]={0};//存储每个点的入度 int queue[510];//存储所求序列 void topo(int n) { int m,t=0; for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++)//每次循环找到第一个入度为0的点 { if(indegree[j]==0) { m=j; break; } } queue[t++]=m;//将入度为0的点存进数组 indegree[m]=-1;//标记已经记录过的点 for(int k=1;k<=n;k++)//将查询到入度为0点相连点的入度减一 { if(map[m][k]) indegree[k]--; } } printf("%d",queue[0]); for(int i=1;i<n;i++) printf(" %d",queue[i]); printf("\n"); } int main() { int n,m; scanf("%d%d",&n,&m); for(int i=0;i<m;i++) { int a,b; scanf("%d%d",&a,&b); if(map[b][a]==0)//防止数据重复 { map[b][a]=1; indegree[a]++; } } topo(n); return 0; }
二:邻接表
1 #include<stdio.h> 2 3 #include<iostream> 4 5 #include<string.h> 6 7 using namespace std; 8 9 int indegree[510]={0}; 10 11 int queue[510]; 12 13 struct Node 14 15 { 16 17 int next; 18 19 int to; 20 21 }A[510]; 22 23 int head[510]; 24 25 void topo(int n) 26 27 { 28 29 int m,t=0; 30 31 for(int i=1;i<=n;i++) 32 33 { 34 35 for(int j=1;j<=n;j++) 36 37 { 38 39 if(indegree[j]==0) 40 41 { 42 43 m=j; 44 45 break; 46 47 } 48 49 } 50 51 queue[t++]=m; 52 53 indegree[m]=-1; 54 55 for(int k=head[m];k!=-1;k=A[k].next) 56 57 { 58 59 indegree[A[k].to]--; 60 61 } 62 63 } 64 65 printf("%d",queue[0]); 66 67 for(int i=1;i<n;i++) 68 69 printf(" %d",queue[i]); 70 71 printf("\n"); 72 73 } 74 75 int main() 76 77 { 78 79 int n,m; 80 81 memset(head,-1,sizeof(head)); 82 83 scanf("%d%d",&n,&m); 84 85 for(int i=1;i<=m;i++) 86 87 { 88 89 int a,b; 90 91 scanf("%d%d",&a,&b); 92 93 A[i].to=a; 94 95 A[i].next=head[b]; 96 97 head[b]=i; 98 99 indegree[a]++; 100 101 102 103 } 104 105 topo(n); 106 107 return 0; 108 109 110 111 } 112 113
三:队列
1 #include<stdio.h> 2 3 #include<iostream> 4 5 #include<queue> 6 7 int map[510][510]={0}; 8 9 int indegree[510]={0}; 10 11 int que[510]; 12 13 using namespace std; 14 15 void topo(int n) 16 17 { 18 19 priority_queue<int,vector<int>,greater<int> >q; 20 21 int m,t=0; 22 23 for(int i=1;i<=n;i++) 24 25 { 26 27 if(indegree[i]==0) 28 29 q.push(i); 30 31 } 32 33 34 35 while(!q.empty()) 36 37 { 38 39 int top=q.top(); 40 41 q.pop(); 42 43 indegree[top]=-1; 44 45 que[t++]=top; 46 47 for(int j=1;j<=n;j++) 48 49 { 50 51 if(map[top][j]) 52 53 { 54 55 indegree[j]--; 56 57 if(indegree[j]==0) 58 59 q.push(j); 60 61 } 62 63 } 64 65 66 67 } 68 69 printf("%d",que[0]); 70 71 for(int i=1;i<n;i++) 72 73 printf(" %d",que[i]); 74 75 printf("\n"); 76 77 } 78 79 int main() 80 81 { 82 83 int n,m; 84 85 scanf("%d%d",&n,&m); 86 87 for(int i=0;i<m;i++) 88 89 { 90 91 int a,b; 92 93 scanf("%d%d",&a,&b); 94 95 if(map[b][a]==0) 96 97 { 98 99 map[b][a]=1; 100 101 indegree[a]++; 102 103 } 104 105 } 106 107 topo(n); 108 109 return 0; 110 111 }
运行截图
以下是用第三种方法队列运行程序得到的结果:
个人总结
第一种方法简单易懂,但缺点是用二维数组存储数据时容易超内存,而题目往往要求的数据较大,大多数题目不能用这种方法解题。第二种方法用邻接表,可以实现大量数据的存储,但邻接表每个节点的查询较难看懂,最好在纸上推演。有些题目不仅要求拓扑排序,还会有其他的要求,对于每个数据有不同的权重,队列可以完美地解决这类问题,例题http://acm.hust.edu.cn/vjudge/contest/128662#problem/A。
这三种方法的核心算法思想是一样的,只是在具体的实现过程中用了不同的数据存储方式,可以适应不同的情况。