1. 程式人生 > >三、【圖演算法】DFS應用-拓撲排序

三、【圖演算法】DFS應用-拓撲排序

深度優先搜尋(DFS)演算法是最重要的圖遍歷演算法,基於DFS框架,可以匯出大量的圖演算法,圖的拓撲排序即為其中一個很典型的例子。

拓撲排序:給定一個有向圖,如何在保證“每個頂點都不會通過邊,指向其在此序列中的前驅頂點”這一前提下,將所有頂點排成一個線性序列。

例如

在編寫教材時,由於各個知識點之間具有一定的依賴關係,如何將這些知識點串聯為一份教學計劃,保證在整個授課程序中,每節課的基本知識點均在之前已講授呢?

這個例子就是個典型的拓撲排序例項。

特點:有向無環圖的拓撲排序一定存在。

策略:在DFS中首先轉換至VISITED狀態的頂點是出度為0的頂點m,此頂點m對之後的搜尋沒有任何作用,這時就相當於拓撲排序的最後一個頂點,將頂點m剔除,之後的出度為0的頂點即為頂點m的前驅,這樣將DFS遍歷結束,即可將這些前後剔除的頂點連線起來構成一個拓撲排序。注意,多個連通域會執行多次DFS,這些DFS各自剔除的頂點之前其實是沒任何關係的,所以可以任意排列。

實現:由於整個圖可能具有多個連通域,從單個頂點開始的TSort可能不能遍歷到圖中的所有頂點,所以TSort函式能夠遍歷從頂點s開始的單個連通域,而tSort函式則對所有頂點進行檢查,只要未曾被訪問過,就從該點開始一次新的TSort搜尋,這樣就能保證所有的連通域都能夠被遍歷到。

本文使用的圖資料結構參見之前部落格https://blog.csdn.net/qq_18108083/article/details/84870399

template<typename Tv, typename Te> bool graph<Tv, Te>::TSort(int v, int& clock, stack<Tv>* S)   //(基於DFS)單個連通域的拓撲排序,從連通域的任一頂點開始即可,因為會有外層tSort函式排查尚未發現的頂點
{
	status(v) = DISCOVERED;       
	dTime(v) = ++clock;
	for (int u = firstNbr(v); u > -1; u = nextNbr(v, u))
	{
		switch (status(u))
		{
		case UNDISCOVERED:   //若此頂點尚未被發現
			status(u) = DISCOVERED;  //標記為已被發現
			type(v, u) = TREE;     //標記邊e(v,u)為遍歷樹的枝幹
			parent(u) = v;
			if (!TSort(u, clock, S))  //繼續深入遍歷
				return false;
			break;
		case DISCOVERED:    //若此頂點已被發現但尚未完成遍歷,則為迴環
			type(v, u) = BACKWARD;
			return false;  //發現此連通域為有環圖,不能生成拓撲序列
		default:   //VISITED  發現已經遍歷完畢的頂點
			type(v, u) = (dTime(v) < dTime(u)) ? FORWARD : CROSS;  //根據頂點最開始遍歷的時間標籤dTime判斷是前向邊還是兩個分支的跨邊
			break;
		}
	}  
	//此頂點已經完成遍歷
	status(v) = VISITED;
	S->push(vdata(v));
	return true;
}

template<typename Tv, typename Te> stack<Tv>* graph<Tv, Te>::tSort(int s)
{
	reset();
	stack<Tv>* S=new stack<Tv>;
	int v = s;
	int clock = 0;
	do
	{
		if (status(v) == UNDISCOVERED)
		{
			if (!TSort(v, clock, S))   //如果發現是有環圖不能生成拓撲序列,則返回
			{
				while (!(S->empty()))
				{
					S->pop(); 
				}
				break;
			}
		}
	} while ((v=(++v)%n)!=s);
	return S;
}

效率:若圖G=(V,E)中共有n個頂點和e條邊,則tSort僅需O(n+e)時間。