天天看點

【資料結構】AOV網——拓撲排序

相關概念

AOV網

AOV網(Activity On Vertex Network)用頂點表示活動。邊是無權的,僅僅用來表示前驅與後繼關系。

前驅與後繼

有向邊的起點稱為終點的前驅,有向邊的終點稱為起點的後繼。拓撲排序的關注點在于前驅——一個結點的前驅結點全部被通路過後,該結點才能被通路。

拓撲序列

對一個有向無環圖G進行拓撲排序,是将G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊<u,v>∈E(G),則u線上性序列中出現在v之前。

>_< 看個圖了解一下上面的定義吧!!!

【資料結構】AOV網——拓撲排序

拓撲排序算法(DFS)

▼ 課程表是否合理(是否存在拓撲序列?有向圖是否有環?)

【DFS判環】
 思路: 在DFS過程中标記染色,如果出現閉環立即判錯
 使用一個visted[]數組(我更喜歡稱其為draw數組):
     	*  0   表示未被通路過
     	*  1   表示正在通路中,即在"本次"DFS路徑中正在被通路,當再次通路到該處,說明出現閉環
     	* -1   表示"絕對安全",即某次DFS直到結束也沒有出現閉環,當再次通路到該處,直接可以認為從該處開始沒有閉環


private boolean hasLoop(List<List<Integer>> graph, int n) {
	// 多從DFS共用一個draw數組
	int[] draw = new int[n];
	// 考慮到圖可能不完全連通,需要從每個點開始一次DFS
	for (int i = 0; i < n; i++) {
		if (!DFS(graph, i, draw)) {
			return false;
		}
	}
	return true;
}

/**
 * 0  表示未被通路過
 * 1  表示正在通路中,即在本次DFS路徑中正在被通路,當再次通路到該處,說明出現閉環
 * -1 表示絕對安全,即某次DFS直到結束也沒有出現閉環,當再次通路到該處,直接可以認為從該處開始沒有閉環
*/
private boolean DFS(List<List<Integer>> graph, int cur, int[] draw) {
	if (draw[cur] == 1) {               // 出現閉環,直接判false
		return false;
	}
	if (draw[cur] == -1) {              // 從該點開始絕對安全,直接判true
		return true;
	}
	draw[cur] = 1;                      // cur點标記為正在本次DFS路徑中
	for (int nxt : graph.get(cur)) {
		if (!DFS(graph, nxt, draw)) {
			return false;
		}
	}
	draw[cur] = -1;                     // 從cur開始的DFS徹底結束,标記cur點絕對安全
	return true;
}
           

▼ 排出一個合理的課程表(生成拓撲序列)

【DFS生成拓撲序列】
 由上面的DFS代碼轉化而來:
	 1. 用一個成員變量記錄是否有環
	 2. 用一個棧結構存放拓撲序列
	 3. 每次判定出一個結點"絕對安全"時,就可以入棧。


private int[] topologicalSort(List<List<Integer>> graph, int n) {
	// 多從DFS共用一個draw數組
	int[] draw = new int[n];
	// 考慮到圖可能不完全連通,需要從每個點開始一次DFS
	for (int i = 0; i < n; i++) {
		DFS(graph, i, draw);
	}
	// 取出拓撲序列
	if (hasLoop) {
		return new int[0];
	} else {
		int[] res = new int[n];
		int index = 0;
		while (!stack.isEmpty()) {
			res[index] = stack.pop();
		}
		return res;
	}
}

private ArrayDeque<Integer> stack = new ArrayDeque<>();     // 用棧記錄拓撲序列
private boolean hasLoop = false;                            // 是否有環

private void DFS(List<List<Integer>> graph, int cur, int[] draw) {
	if (draw[cur] == 1) {
		hasLoop = true;
		return;
	}
	if (draw[cur] == -1) {
		return;
	}
	draw[cur] = 1;
	for (int nxt : graph.get(cur)) {
		DFS(graph, nxt, draw);
	}
	draw[cur] = -1;
	stack.push(cur);    // 絕對安全後,cur入棧
}
           

拓撲排序算法(BFS)

▼ 課程表是否合理(是否存在拓撲序列?有向圖是否有環?)

【BFS判環】
 思路: 使用degree[]入度數組,一個結點入度為0說明它的前驅結點都已完成
      	1. 初始化入度數組
      	2. 入度為0的結點先入隊
      	3. 開始BFS,每取出一個結點即學習完一個結點,将它指向的所有結點入度減1
      	4. 如果入度減一後變為0,則該結點入隊
      	5. 每次進行入隊操作時,便計數+1,如果最後入隊的次數等于總結點數,說明所有結點都已完成,無環


private boolean hasLoop(List<List<Integer>> graph, int n) {

	// 初始化入度數組
	int[] degree = new int[n];
	for (int i = 0; i < n; i++) {
		for (int j : graph.get(i)) {
			degree[j]++;
		}
	}

	// 初始化隊列(将入度為0的結點加入隊列中)
	ArrayDeque<Integer> queue = new ArrayDeque<>();
	for (int i = 0; i < n; i++) {
		if (degree[i] == 0) {
			queue.offer(i);
		}
	}

	// BFS,每取出一個結點即學習完一個結點,将它指向的所有結點入度減1
	int count = 0;
    while (!queue.isEmpty()) {
		int cur = queue.poll();
			for (int nxt : graph.get(cur)) {
				degree[nxt]--;
				if (degree[nxt] == 0) {
					queue.offer(nxt);
					count++;
				}
			}
	}

	// 如果所有結點都被通路過(學習過),則說明存在拓撲序列
	return count == n;
	
}
           

▼ 排出一個合理的課程表(生成拓撲序列)

【BFS判環】
 由上面的BFS代碼轉化而來
	1. 用一個暫存隊列記錄拓撲序列(别和DFS用的隊列搞混了)
	2. 每次有結點入隊時,就将它加到暫存隊列中,最後再把暫存隊列轉化為拓撲序列
	3. 如果隊列的長度等于結點的總個數,則傳回序列;否則說明有環,無合法拓撲序列


private boolean hasLoop(List<List<Integer>> graph, int n) {

	// 初始化入度數組
	int[] degree = new int[n];
	for (int i = 0; i < n; i++) {
		for (int j : graph.get(i)) {
			degree[j]++;
		}
	}

 	// 最終結果,下标兼具計數的作用
	int[] res = new int[n];
	int count = 0;

	// 初始化隊列(将入度為0的結點加入隊列中)
	ArrayDeque<Integer> queue = new ArrayDeque<>();
	for (int i = 0; i < n; i++) {
		if (degree[i] == 0) {
			queue.offer(i);
			res[count++] = i;
		}
	}

	// BFS,每取出一個結點即學習完一個結點,将它指向的所有結點入度減1
    while (!queue.isEmpty()) {
		int cur = queue.poll();
			for (int nxt : graph.get(cur)) {
				degree[nxt]--;
				if (degree[nxt] == 0) {
					queue.offer(nxt);
					res[count++] = nxt;
				}
			}
	}

	// 如果隊列的長度等于結點的總個數,則傳回序列;否則說明有環,無合法拓撲序列
	return count == n ? res : new int[0];
	
}
           

補充說明

❶ 我們發現,DFS和BFS是兩種不同的思路——【DFS+染色數組】VS【BFS+入度數組】

❷【生成拓撲序列】的代碼實作直接在【判環】的代碼基礎上修改就行了

❸ 生成拓撲序列時,我們應當意識到DFS和BFS的一個重要差別:

  • DFS是一個逆向生成序列的過程。 比如1 → 3 → 5,在DFS時,從1開始遞歸,但1最後完成遞歸。我們會依次得到5,3,1,是以這個記錄序列的資料結構我們使用棧(Stack)
  • BFS是一個正向生成序列的過程。 比如1 → 3 → 5,在BFS時,入度為0的1先入隊,之後3,5依次入度減為0而入隊。我們會依次得到1,3,5,是以這個記錄序列的資料結構我們使用隊列(Queue)

☘️

繼續閱讀