一个比较大的工程往往会被划分成多个子工程进行, 我们把这些子工程称之为活动。在整个工程中,有些子工程必须在其他一些相关的子工程完成之后才能开始,就是说一个子工程的开始依赖于另外一个子工程的结果,使得一个子工程的结束成为了另外一个子工程开始的先决条件。但是也有一些子工程没有这样的制约性的先决条件,它可以在整个工期的任何时候开始和结束。我们可以通过一个有向图来表示出整个工程中所有子工程之间的先后关系,用图中的顶点表示子工程,图中的有向边代表子工程之间的先后关系,即图中有向边的起点所代表的活动是有向边终点所代表活动的先决工程,只有当起点的工程完成后才能开始终点对应的工程,没有边相连接的两个子工程之间代码没有先后关系。我们把这种顶点表示活动, 边表示活动之间的先后关系的有向图叫做顶点活动网(Activity On Vertex network)也叫AOV网。 如下图是一个电影制作的AOV网:
在上面电影制作的全过程被分解成很多的小工程进行处理, 其中有些工程,比如人员到位进驻场地之前必须确定好场地,演员等。这就是先决条件,而剧本完善和演员确定之间可以同时进行,他们并没有直接的先后连接关系。这样一张图就反应了一个电影制作的过程,这也是一张标准的AOV网。
AOV网是一个有向的无环图,不应该带有回路也就是环,否则就会出现自己是自己的先决条件的笑话,使得每一个活动都无法进行从而形成死锁。对于一个AOV网,我们可以把所有的子活动都排列成一个线性的序列, 使其严格满足每个活动的先决活动都排在该活动的前面,这样一个序列叫做拓扑序列(Topological order),通过AOV网构造拓扑序列的过程叫做拓扑排序,注意拓扑排序后的拓扑序列并不是唯一的,因为有很多活动是可以同时执行的。拓扑排序只能保证有先后顺序关系的活动严格按照先后顺序执行。
拓扑排序(Topological sort)的过程是这样的:
1>从所有顶点中找到一个顶点入度为0的顶点。这个要依赖于前期有对每个顶点的入度有所记录。
2>删除第一步找到的入度为0的顶点为出发点的所有弧,并且将弧所指向的的顶点的入度减一。
3>重复以上两步,直到所有的顶点的入度均为0即停止。在上述过程中每次找到入度为0的顶点将其输出,最终输出的便是一个拓扑序列。此过程可以借助stack或者queue进行。
邻接表表示的拓扑排序过程如下所示:
typedef struct edge
{
int AdjVertex;
edge* Next;
}EdgeNode;
typedef struct vertex
{
int InDegree;
int Infos;
EdgeNode* FirstEdge;
}VertexNode, AdjList[100];
typedef struct graph
{
AdjList adjlist;
int vernum, edgenum;
}Graph;
void TopologicalSort(Graph* g)
{
int count = 0;
VertexNode node;
EdgeNode* tmp = NULL;
stack<VertexNode> stackver; // 辅助栈结构
// 遍历所有顶点,将顶点入度为0的所有顶点都加入栈
for (int i = 0; i < g->vernum; i++)
{
if (g->adjlist[i].InDegree == 0)
{
stackver.push(g->adjlist[i]);
}
}
// 循环处理所有顶点入度为0的顶点
while(!stackver.empty())
{
node = stackver.top();
stackver.pop();
count++;
cout << node.Infos << endl;
int index;
tmp = node.FirstEdge;
while(tmp != NULL) // 遍历顶点为入度为0的顶点,并且处理和其相关的顶点
{
index = tmp->AdjVertex;
g->adjlist[index].InDegree--; // 以该顶点为起点的顶点入度减1
if (g->adjlist[index].InDegree == 0)
{ // 判断相关连的顶点是否成为新的入度为0的点,满足条件进行入栈
stackver.push(g->adjlist[index]);
}
}
}
// 当所有顶点都处理为入度为0,拓扑排序完成
if (count == g->vernum)
{
cout << "Topological sort succeed" << endl;
return;
}
// 尚有顶点无法处理说明不是AOV网,是有向带环图
cout << "Topological sort failed" << endl;
}
邻接矩阵的拓扑排序代码:
#define MaxNum 501
int InMark[MaxNum+1];
int map[MaxNum+1][MaxNum+1];
void TopologicalSort()
{
int cnt = 0;
while(cnt < MaxNum)
{
int index = 0;
for(int i = 1; i <= MaxNum; i++)
{
// 查找入度为0的顶点
if(InMark[i] == 0)
{
index = i;
break;
}
}
cnt++;
InMark[index] = -1; // 标记入度为0的顶点已经处理
cout << index ;
if (cnt != MaxNum) cout <<" ";
// 处理入度为0的顶点相关的顶点
for(int i = 1; i <= MaxNum; i++)
{
if(map[index][i] == 1)
InMark[i]--;
}
}
cout << endl;
}
利用DFS求拓扑序列的抽象算法可描述为:
void DFSTopSort(G,i,T){
// i是搜索的出发点,T是栈
int j;
visited[i]=TRUE; //访问i
for(所有i的邻接点j) //即<i,j>∈E(G)
if(!visited[j])
DFSTopSort(G,j,T);
//以上语句完全类似于DFS算法
Push(&T,i); //从i出发的搜索已完成,输出i
}
DFS版本拓扑排序:
typedef struct edge
{
int AdjVertex;
edge* Next;
}EdgeNode;
typedef struct vertex
{
int InDegree;
int Infos;
EdgeNode* FirstEdge;
}VertexNode, AdjList[100];
typedef struct graph
{
AdjList adjlist;
int vernum, edgenum;
}Graph;
int Mark[101]; // 标记顶点状态
stack<int> Toporder; // 存放生成的拓扑逆序
void DFS(Graph* g, int cur_ver)
{
Mark[cur_ver] = -1; // -1顶点处于访问状态
EdgeNode* tmp = NULL;
// tmp指向对应的顶点的邻接顶点
tmp = g->adjlist[cur_ver].FirstEdge;
// 循环递归邻接顶点
while(tmp != NULL)
{
if(Mark[tmp->AdjVertex] == 0)// 当前顶点未被访问
{
DFS(g, tmp->AdjVertex);
}
else if (Mark[tmp->AdjVertex] == -1)
{
// 如果邻接顶点正在访问 说明有环
cout << "Graph exist loop" << endl;
return ;
}
}
// 将遍历生成的顶点加入栈保存
Toporder.push(cur_ver);
Mark[cur_ver] = 1; // 标记顶点已经处理过
}
void TopologicalSort(Graph* g)
{
memset(Mark, 0x00, sizeof(Mark));
for (int i = 0; i < g->vernum; i++)
{
if (Mark[i] == 0)
DFS(g, 1);
}
// 打印拓扑序列
while(!Toporder.empty())
{
cout << Toporder.top() << " ";
Toporder.pop();
}
cout << endl;
}
当然你也可以根据上面邻接矩阵的思想不采用递归的方式去实现:
#define MaxNum 501
int InMark[MaxNum+1];
for (int i - 1; i < MaxNum; i++)
{
int j = 1;
for (j = 1; j < MaxNum; j++)
{
if (InMark[j] == 0)
{
break;
}
}
if (j == MaxNum) break;
InMark[j] = 1;
for (int k = 1; k <= MaxNum; k++)
{
if (map[j][k]>0)
InMark[k]--;
}
}