1. 程式人生 > 其它 >【LeetCode】課程表(圖論判環 拓撲排序/dfs)

【LeetCode】課程表(圖論判環 拓撲排序/dfs)

課程表( 拓撲排序/dfs 判環)

題目連結:https://leetcode-cn.com/problems/course-schedule/

題目大意:給定一個課程依賴關係圖,比如課程A依賴課程B,課程B依賴課程C,按照上述的依賴關係,能否學習完所有的課程?

先學C,再學B,最後學A即可

方式1:拓撲排序

我們按照圖論的思想,將每個課程看做一個節點,將課程間的這種依賴和被依賴的關係看做節點的出度和入度,即

依賴:出度

被依賴:入度

樣例如下

A依賴B等價於A節點指向B節點,那麼A節點的出度是1,B節點的入度是1

A依賴D等價於A節點指向B節點,那麼A節點現在的出度是2,D節點的入度是1

我們首先找到所有入度為0的節點,即這些節點不被任何節點依賴,我們可以先完成這些課程nums1,這些課程被完成後,依賴這些課程的節點nums2其入度也需要被修改,具體的修改操作為nums2節點的入度都需要減去1,因為nums1節點課程都完成了

然後我們繼續選取入度為0的點,直到沒有入度為0的點

對於這些入度為0的點的儲存我們可以採用佇列,先進先出

當佇列中沒有元素後,即所有入度為0的點都被處理了,如果處理的點總數等於節點總數,那麼證明可以存在一個拓撲順序,學習完所有課程,如果不等於,那麼證明圖中存在環,互相依賴死迴圈了,就不能學習完所有課程

這種方法叫做拓撲排序,出入佇列元素的先後順序就是拓撲排序的順序

時間複雜度:O(n+m)

  • n 為課程數,m 為先修課程的要求數。這其實就是對圖進行廣度優先搜尋的時間複雜度。

空間複雜度:O(n+m)

  • 雙層map儲存圖需要O(m),佇列儲存需要O(n)

func canFinish(numCourses int, prerequisites [][]int) bool {
	G:=make(map[int]map[int]int)

	indegree:=make(map[int]int)

	var queue []int

	// 構建圖和入度表
	for i:=0;i<len(prerequisites);i++{
		v1:=prerequisites[i][0]
		v2:=prerequisites[i][1]
		if _,ok:=G[v1];!ok{
			G[v1]=make(map[int]int)
		}
		G[v1][v2]=1
		indegree[v2]++
	}

	// 入度為0的點加入佇列
	for i:=0;i<numCourses;i++{
		if indegree[i]==0{
			queue=append(queue,i)
		}
	}

	c:=0
	for len(queue)!=0{
		temp:=queue[0]
		queue=queue[1:]

		// 符合拓撲排序的節點數量
		c++

		// temp 和 i 存在邊,則i的入度減1,如果i入度為0,則加入佇列
		for i:=0;i<numCourses;i++{
			if G[temp][i]==1{
				indegree[i]--
				if indegree[i]==0{
					queue=append(queue,i)
				}
			}
		}

	}

	// 符合拓撲排序節點的數量等於節點總數量,證明不存在環,符合要求
	if c==numCourses{
		return true
	}
	return false

}

方式2:dfs

拓撲排序的方法是基於入度為0的點考慮的,其實我們也可以基於出度為0的點考慮

出度為0意味著此節點不依賴其他節點,只可能被其他節點依賴

那如何尋找到出度為0的節點呢?

答案就是深度優先搜尋,dfs

有路徑就一直搜尋下去,深度優先

對於每個節點u,我們可以定義0,1,2三種狀態

  • 0 未搜尋

    • 代表此節點還沒有被搜尋過
  • 1 搜素中

    • 我們搜尋過這個節點,但還沒有回溯到該節點即該節點還沒有入棧,還有相鄰的節點沒有搜尋完成
  • 2 搜素已完成

    • 我們搜尋過並且回溯過這個節點,即該節點已經入棧,並且所有該節點的相鄰節點都出現在棧的更底部的位置,滿足拓撲排序的要求。

我們搜素一個節點x時,會搜尋x指向的所有節點

  • 如果這些節點沒有被搜尋過,那麼搜尋它
  • 如果這些節點處於搜尋中,也就是被其他dfs搜尋過了,那麼證明此圖中存在環!
  • 如果此節點處於搜尋已完成,那麼跳過

時間複雜度:O(n+m), n是課程數量,m是依賴數量

空間複雜度:O(n+m) 儲存圖需要O(m),遞迴需要O(n)

var G map[int]map[int]int
var flag [100005]int
var N int
var result bool

func dfs(index int){
	// index標記為已搜尋,但是還在搜尋和index有關的其他節點
	flag[index]=1

	for i:=0;i<N;i++{
		if G[index][i]==1{
			if flag[i]==0{
				// i沒有被搜尋過,搜尋i
				dfs(i)
				if result==false{
					return
				}
			}else if flag[i]==1{
				// i已經被其他dfs標記過了,證明存在環,沒有合法拓撲排序
				result=false
				return
			}else if flag[i]==2 {
				continue
				// i 此時已經在拓撲排序中,無需做任何操作
			}
		}
	}

	// index標記為加入到了拓撲排序中
	flag[index]=2
}
func canFinish(numCourses int, prerequisites [][]int) bool {
	G = make(map[int]map[int]int)
	flag=[100005]int{}
	N=numCourses
	result=true

	// 構建圖
	for i:=0;i<len(prerequisites);i++{
		v1:=prerequisites[i][0]
		v2:=prerequisites[i][1]
		if _,ok:=G[v1];!ok{
			G[v1]=make(map[int]int)
		}
		G[v1][v2]=1
	}


	// 對每一個沒有被搜尋過的節點都進行一次搜尋
	for i:=0;i<numCourses;i++{
		if flag[i]==0{
			dfs(i)
		}
	}

	return result
}