拓撲排序(Topological Sort)

         一個比較大的工程往往會被劃分成多個子工程進行, 我們把這些子工程稱之爲活動。在整個工程中,有些子工程必須在其他一些相關的子工程完成之後才能開始,就是說一個子工程的開始依賴於另外一個子工程的結果,使得一個子工程的結束成爲了另外一個子工程開始的先決條件。但是也有一些子工程沒有這樣的制約性的先決條件,它可以在整個工期的任何時候開始和結束。我們可以通過一個有向圖來表示出整個工程中所有子工程之間的先後關係,用圖中的頂點表示子工程,圖中的有向邊代表子工程之間的先後關係,即圖中有向邊的起點所代表的活動是有向邊終點所代表活動的先決工程,只有當起點的工程完成後才能開始終點對應的工程,沒有邊相連接的兩個子工程之間代碼沒有先後關係。我們把這種頂點表示活動, 邊表示活動之間的先後關係的有向圖叫做頂點活動網(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;
}


  對於有n個頂點和e條邊的有向圖,建立個頂點的入度的時間複雜度爲O(e),建立入度爲0的頂點棧的時間複雜度爲O(n);在拓撲排序的過程中,若每個無向圖的無環,則每個頂點出入棧各一次,入度減一在while語句中共執行e次。所以時間複雜度總的爲O(n+e),對其優化也可以使用優先隊列替換掉stack,使得速度加快。不過當有向圖無環時們也可以利用深度優先遍歷進行拓撲排序,因爲沒有環,所以從圖中某個頂點進行DFS時,最先找出的頂點是頂點出度爲0的頂點。也就是拓撲序列中最後一個頂點,所以DFS記錄下來的是拓撲序列的逆向

利用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]--;
	}
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章