拓扑排序和关键路径
一、拓撲排序
什么是拓撲排序?簡單的說,由某個集合上的一個偏序得到該集合上的一個全序,這個操作稱之為拓撲排序。
回顧離散數學中關于偏序和全序的定義:
若集合X上的關系R是自反的,反對稱的和傳遞的,則稱R是集合X上的偏序關系。
設R是集合X上的偏序,如果對每個x,y∈X必有xRy或yRx,則稱R是集合X上的全序關系。
直觀地看,偏序指集合中僅有部分成員之間可比較,而全序指集合中全體成員之間均可比較下圖所示的兩個有向圖,圖中弧(x,y)表示x≤y,則(a)表示偏序,(b)表示全序。若在(a)的有向圖上人為地加一個表示v2≤v3的弧(符號“≤”表示v2領先于v3),則(a)表示的亦為全序,且這個全序稱為拓撲有序(Topological Order),而由偏序定義得到拓撲有序的操作便是拓撲排序。
如何進行拓撲排序?
? 一、從有向圖中選取一個沒有前驅的頂點,并輸出之;
? 二、從有向圖中刪去此頂點以及所有以它為尾的弧;
? 重復上述兩步,直至圖空,或者圖不空但找不到無前驅的頂點為止。沒有前驅 — 入度為零,刪除頂點及以它為尾的弧– 弧頭頂點的入度減1。
AOV-網及其拓撲有序序列產生的過程
(a)AOV-網;(b)輸出v6之后;(c)輸出v1之后;(d)輸出v4之后;(e)輸出v3之后;(f)輸出v2之后
最后得到該有向圖的拓撲有序序列為:
v6- v1- v4- v3- v2- v5
?
代碼實現:
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_VERTEX_NUM 20 //圖的最大頂點數 #define MAX 30 //棧的最大容量 enum BOOL {False,True}; typedef struct ArcNode {int adjvex; //該弧所指向的頂點的位置struct ArcNode *nextarc; //指向下一條弧的指針 } ArcNode; //弧結點 typedef struct {int indegree[MAX_VERTEX_NUM]; //存放各頂點的入度ArcNode* AdjList[MAX_VERTEX_NUM]; //指向第一條依附該頂點的弧的指針int vexnum,arcnum; //圖的當前頂點和弧數 } Graph; typedef struct //定義堆棧結構 {int elem[MAX]; //棧區int top; //棧頂指針 } Stack; void CreateGraph ( Graph & ); //生成圖的鄰接表 BOOL TopologicalSort ( Graph ); //進行拓撲排序 void FindInDegree ( Graph& ); //求圖各頂點的入度 void Initial ( Stack & ); //初始化一個堆棧 BOOL Push ( Stack &,int ); //將一個元素入棧 BOOL Pop ( Stack&,int & ); //將一個元素出棧 BOOL StackEmpty ( Stack ); //判斷堆棧是否為空 void main() {Graph G; //采用鄰接表結構的圖char j='y';BOOL temp;while ( j!='N'&&j!='n' ){CreateGraph ( G ); //生成鄰接表結構的圖temp=TopologicalSort ( G ); //進行拓撲排序if ( temp==False ) printf ( "該圖有回路!\n" );//若返回為False,表明該圖存在有環路else printf ( "該圖沒有回路!\n" );printf ( "拓撲排序完畢,繼續進行嗎?(Y/N)" );scanf ( " %c",&j );} }void CreateGraph ( Graph &G ) {//構造鄰接表結構的圖Gint i;int start,end;ArcNode *s;printf ( "請輸入圖的頂點數和弧數:" );scanf ( "%d,%d",&G.vexnum,&G.arcnum ); //輸入圖的頂點數和弧數for ( i=1; i<=G.vexnum; i++ ) G.AdjList[i]=NULL; //初始化指針數組printf ( "請輸入各弧, 格式:弧尾,弧頭\n" );for ( i=1; i<=G.arcnum; i++ ){scanf ( "%d,%d",&start,&end ); //輸入弧的起點和終點s= ( ArcNode * ) malloc ( sizeof ( ArcNode ) ); //生成一個弧結點s->nextarc=G.AdjList[start]; //插入到鄰接表中s->adjvex=end;G.AdjList[start]=s;} } BOOL TopologicalSort ( Graph G ) {//對圖G進行拓撲排序,若G存在回路,返回False,否則返回Trueint i,k;int count; //計數器ArcNode* p;Stack S;FindInDegree ( G ); //求出圖中各頂點的入度Initial ( S ); //堆棧初始化,存放入度為零的頂點for ( i=1; i<=G.vexnum; i++ )if ( !G.indegree[i] ) Push ( S,i ); //入度為零的頂點入棧count=0; //對輸出頂點記數printf ( "拓撲排序:" );while ( !StackEmpty ( S ) ){Pop ( S,i ); //輸出i號頂點并記數printf ( "%d->",i );count++;for ( p=G.AdjList[i]; p; p=p->nextarc ){k=p->adjvex; //對i號頂點的每個鄰接頂點的入度減一if ( ! ( --G.indegree[k] ) ) Push ( S,k ); //若入度為零,入棧}}printf ( "\b\b \n" );if ( count<G.vexnum ) return False; //如輸出頂點數少于圖中頂點數,則該圖有回路else return True; } void FindInDegree ( Graph &G ) {//求出圖G的各頂點的入度,存放在G.indegree[1..G.vexnum]中int i;ArcNode* p;for ( i=1; i<=G.vexnum; i++ )G.indegree[i]=0;for ( i=1; i<=G.vexnum; i++ ){for ( p=G.AdjList[i]; p; p=p->nextarc )G.indegree[p->adjvex]++; //弧頭頂點的入度加一} } void Initial ( Stack &S ) {S.top=-1; //棧頂指針初始化為-1 }BOOL Push ( Stack &S,int ch ) {//將元素ch入棧,成功返回True,失敗返回Falseif ( S.top>=MAX-1 ) return False;//判斷是否棧滿else{S.top++; //棧頂指針top加一S.elem[S.top]=ch; //入棧return True;} }BOOL Pop ( Stack &S,int &ch ) {//將棧頂元素出棧,成功返回True,并用ch返回該元素值,失敗返回Falseif ( S.top<=-1 ) return False;//判斷是否棧空else{S.top--; //棧頂指針減一ch=S.elem[S.top+1];return True;} } BOOL StackEmpty ( Stack S ) {//判斷堆棧是否已空,若空返回True,不空返回Falseif ( S.top<=-1 ) return True;else return False; }
行結果:
二、關鍵路徑
重要概念:
AOE (Activity On Edges)網絡?如果在無有向環的帶權有向圖中用有向邊表示一個工程中的各項活動(Activity),用邊上的權值表示活動的持續時間(Duration),用頂點表示事件(Event),則這樣的有向圖叫做用邊表示活動的網絡,簡稱AOE (Activity On Edges)網絡。AOE網是一個帶權的有向無環圖。
AOE網絡在某些工程估算方面非常有用。例如,可以使人們了解:
??? a、完成整個工程至少需要多少時間(假設網絡中沒有環)?
??? b、為縮短完成工程所需的時間, 應當加快哪些活動?
關鍵路徑(Critical Path)?在AOE網絡中, 有些活動順序進行,有些活動并行進行。從源點到各個頂點,以至從源點到匯點的有向路徑可能不止一條。這些路徑的長度也可能不同。完成不同路徑的活動所需的時間雖然不同,但只有各條路徑上所有活動都完成了,整個工程才算完成。因此,完成整個工程所需的時間取決于從源點到匯點的最長路徑長度,即在這條路徑上所有活動的持續時間之和。這條路徑長度最長的路徑就叫做關鍵路徑(Critical Path)。
如圖1就是一個AOE網,該網中有11個活動和9個事件。每個事件表示在它之前的活動已經完成,在它之后的活動可以開始。如事件v5表示a4和a5活動已經完成,a7和a8活動可以開始。每個弧上的權值表示完成相應活動所需要的時間,如完成活動a1需要6天,a8需要7天。
AOE網常用于表示工程的計劃或進度。由于實際工程只有一個開始點和一個結束點,因此AOE網存在唯一的入度為0的開始點(又稱源點)和唯一的出度為0的結束點(又稱匯點),例如圖1的AOE網從事件v1開始,以事件v9結束。同時AOE網應當是無環的。
那么該工程待研究的問題是:1.完成整項工程至少需要多少時間?2.哪些活動是影響工程進度的關鍵?
由于在AOE-網中有些活動可以并行進行,所以完成工程的最短時間是從開始點到完成點的最長路徑的長度(這里所說的路徑長度是指路徑上各活動持續時間之和,不是路徑上弧的數目)。路徑長度最長的路徑叫做關鍵路徑(Critical path)。
假設開始點是v1,從v1到vi的最長路徑叫做時間vi的最早發生時間。這個時間決定了所有以vi為尾的弧所表示的活動的最早開始時間。我們用e(i)表示活動ai的最早開始時間。還可以定義一個活動開始的最遲時間l(i),這是在不推遲整個工程完成的前提下,活動ai最遲必須開始進行的時間。兩者之差l(i)-e(i)意味著完成活動ai的時間余量。當這個時間余量等于0的時候,也即是l(i)=e(i)的活動,我們稱其為關鍵活動。顯然,關鍵路徑上的所有活動都是關鍵活動,因此提前完成非關鍵活動并不能加快工程的進度。
因此,分析關鍵路徑的目的是辨別哪些是關鍵活動,以便爭取提高關鍵活動的功效,縮短整個工期。
?
如何實現關鍵路徑?
由上面的分析可知,辨別關鍵活動就是要找e(i)=l(i)的活動。為了求得e(i)和l(i),首先應求得事件的最早發生時間ve(j)和最遲發生時間vl(j)。如果活動ai由弧<j,k>表示,其持續時間記為dut(<j,k>),則有如下關系
e(i) = ve(j)
l(i) = vl(k) – dut(<j,k>)
求解ve(j)和vl(j)需分兩個步進行:
1) 從ve(0)=0開始向前推進求得ve(j)
Ve(j) = Max{ve(i) + dut(<i,j>) };<i,j>屬于T,j=1,2…,n-1
其中T是所有以第j個頂點為頭的弧的集合。
2) 從vl(n-1) = ve(n-1)起向后推進求得vl(j)
vl(i) = Min{vl(j) – dut(<i,j>};<i,j>屬于S,i=n-2,…,0
其中,S是所有以第i個頂點為尾的弧的集合。
這兩個遞推公式的計算必須分別在拓撲有序和逆拓撲有序的前提先進行。也就是說,ve(j-1)必須在vj的所有前驅的最早發生時間求得之后才能確定,而vl(j-1)必須在Vj的所有后繼的最遲發生時間求得之后才能確定。因此可以在拓撲排序的基礎上計算ve(j-1)和vl(j-1)。
?
具體算法描述如下:
1.?輸入e條弧<j,k>,建立AOE-網的存儲結構。
2.?拓撲排序,并求得ve[]。從源點V0出發,令ve[0]=0,按拓撲有序求其余各頂點的最早發生時間ve[i]。如果得到的拓撲有序序列中頂點個數小于網中頂點數n,則說明網中存在環,不能求關鍵路徑,算法終止;否則執行步驟3。
3.?拓撲逆序,求得vl[]。從匯點Vn出發,令vl[n-1] = ve[n-1],按逆拓撲有序求其余各頂點的最遲發生時間vl[i](n-2≥i≥2)。
4.?求得關鍵路徑。根據各頂點的ve和vl值,求每條弧s的最早開始時間e(s)和最遲開始時間l(s)。若某條弧滿足條件e(s) = l(s),則為關鍵活動。
為了能按逆序拓撲有序序列的順序計算各個頂點的vl值,需記下在拓撲排序的過程中求得的拓撲有序序列,這就需要在拓撲排序算法中,增設一個棧,以記錄拓撲有序序列,則在計算求得各頂點的ve值之后,從棧頂到棧底便為逆拓撲有序序列。
?
?
代碼實現:
#include <iostream> #include <fstream> #include <queue> #include <stack> #include <Windows.h>using namespace std;#define INFINITY INT_MAX #define MAX_VERTEX_NUM 20 //頂點最多個數 #define LENGTH 5 //頂點字符長度//鄰接矩陣 typedef struct _Graph {int matrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //鄰接矩陣char vexs[MAX_VERTEX_NUM][LENGTH]; //頂點數組int vexnum; //頂點個數int arcs; //弧的個數 }Graph;int LocateVex(const Graph & g, char name[LENGTH]) {for (int i = 0; i < g.vexnum; i++){if (0 == strcmp(g.vexs[i], name)){return i;}}return -1; }//圖的建造 void CreateGraph(Graph &g) {int i = 0,j = 0;ifstream fcin("criticalpath.txt");fcin>>g.vexnum;for (i = 0; i < g.vexnum; i++){for (j = 0; j < g.vexnum; j++){g.matrix[i][j] = INFINITY;}}for (i = 0; i < g.vexnum; i++){fcin>>g.vexs[i];}fcin>>g.arcs;char arcHead[LENGTH];char arcTail[LENGTH];int weight;for (i = 0; i < g.arcs; i++){memset(arcHead, 0, LENGTH);memset(arcTail, 0, LENGTH);fcin>>arcTail>>arcHead>>weight;int x = LocateVex(g, arcHead);int y = LocateVex(g, arcTail);g.matrix[y][x] = weight;} }//v的第一個鄰接點 int FirstAdjVex(const Graph &g, int v) {for (int i = 0; i < g.vexnum; i++){if (g.matrix[v][i] != INFINITY){return i;}}return -1; }//v相對于w的下一個鄰接點 int NextAdjVex(const Graph &g, int v, int w) {for (int i = w + 1; i < g.vexnum; i++){if (g.matrix[v][i] != INFINITY){return i;}}return -1; }//鄰接矩陣的輸出 void PrintAdjVex(const Graph &g) {for (int i = 0; i < g.vexnum; i++){for (int j = 0; j < g.vexnum; j++){if (g.matrix[i][j] == INFINITY){cout<<"*"<<'\t';}else{cout<<g.matrix[i][j]<<'\t';}}cout<<endl;}cout<<endl; }//************************************關鍵路徑************************************begin int ve[MAX_VERTEX_NUM] = {0}; int vl[MAX_VERTEX_NUM] = {INFINITY}; //查找入度為0的頂點 void FindInDegree(Graph g, int indegree[]) {for (int i = 0; i < g.vexnum; i++){for (int j = 0; j < g.vexnum; j++){if (g.matrix[i][j] != INFINITY){indegree[j]++;}}} }//拓撲排序 bool TopologicalSort(Graph g, stack<int> &s) {int indegree[MAX_VERTEX_NUM] = {0};FindInDegree(g, indegree);queue<int> q;int i = 0;for (; i < g.vexnum; i++){if (indegree[i] == 0){q.push(i);}}int count = 0;while (!q.empty()){i = q.front();s.push(i);q.pop();count++;for (int j = 0; j < g.vexnum; j++){if (g.matrix[i][j] != INFINITY){if (!(--indegree[j])){q.push(j);}if (ve[i] + g.matrix[i][j] > ve[j]){ve[j] = ve[i] + g.matrix[i][j];}}}}if (count < g.vexnum){return false;}else{return true;} }bool CriticalPath(Graph g) {int i = 0, j =0;stack<int> s;if (!TopologicalSort(g, s)){return false;}for (i = 0; i < g.vexnum; i++){vl[i] = INFINITY;}vl[g.vexnum - 1] = ve[g.vexnum - 1];while (!s.empty()){i = s.top();s.pop();for (j = 0; j < g.vexnum; j++){if (g.matrix[i][j] != INFINITY){int tmp = g.matrix[i][j];if (vl[j] - g.matrix[i][j] < vl[i]){vl[i] = vl[j] - g.matrix[i][j];}}}}for (i = 0; i < g.vexnum; i++){for (j = 0; j < g.vexnum; j++){if (g.matrix[i][j] != INFINITY){if (ve[i] == vl[j] - g.matrix[i][j]){cout<<g.vexs[i]<<" "<<g.vexs[j]<<" "<< g.matrix[i][j]<<endl;}}}}cout<<"最長路徑需要"<<vl[g.vexnum - 1]<<"天"<<endl;return true; }//************************************關鍵路徑************************************end//輔助函數,設置控制臺的顏色 void SetConsoleTextColor(WORD dwColor) {HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);if (INVALID_HANDLE_VALUE == handle){return;}SetConsoleTextAttribute(handle, dwColor); }int main(int argc, CHAR* argv[]) {Graph graph;CreateGraph(graph);SetConsoleTextColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY);cout<<"************************鄰接矩陣**************************"<<endl;PrintAdjVex(graph);SetConsoleTextColor(FOREGROUND_GREEN | FOREGROUND_INTENSITY);cout<<"************************關鍵路徑**************************"<<endl<<endl;CriticalPath(graph);cout<<endl<<endl;return 0; }criticalpath.txt文件內容
9
V1 V2 V3 V4 V5 V6 V7 V8 V9
11
V1 V2 6
V1 V3 4
V1 V4 5
V2 V5 1
V3 V5 1
V4 V6 2
V5 V7 9
V5 V8 7
V6 V8 4
V7 V9 2
V8 V9 4
運行結果:
總結
- 上一篇: 有向图强连通分量的三种算法
- 下一篇: 拓扑排序--关键路径