DAG上的動態規劃是學習動態規劃的基礎。不少問題均可以轉化爲DAG上的最長路、最短路或路徑計數問題。算法
也就是一般作題時說的「拓撲排序 + DP」的作法數組
由於最近作的模擬題中常常會出現這樣的題學習
因此我決定學一下並寫一篇博客this
其實我原來學過只是沒學會spa
咱們先從最基礎的講起.net
首先,圖(Graph)描述的是一些個體之間的關係。設計
圖與線性表和二叉樹不一樣的是:這些個體之間既不是前驅後繼的順序關係,也不是祖前後代的層次關係,而是錯綜複雜的網狀關係指針
而DAG就是圖的一種code
DAG(Directed Acyclic Graph)的意思是有向無環圖blog
所謂有向無環圖是指任意一條邊有方向,且不存在環路的圖
一般,咱們把頂點表示活動、邊表示活動間前後關係的有向圖稱作頂點活動網(Activity On Vertex network),簡稱AOV網。
一個AOV網應該是一個DAG,即不該該帶有迴路,由於若帶有迴路,則迴路上的全部活動都沒法進行(對於數據流來講就是死循環)。在AOV網中,若不存在迴路,則全部活動可排列成一個線性序列,使得每一個活動的全部前驅活動都排在該活動的前面,咱們把此序列叫作拓撲序列(Topological order),由AOV網構造拓撲序列的過程叫作拓撲排序(Topological sort)。AOV網的拓撲序列不是惟一的,知足上述定義的任一線性序列都稱做它的拓撲序列。
以上就是一些最基礎的定義
拓撲排序的實現步驟是
1.先把入度爲零的點輸出並在圖中刪除(由於在剛開始的一個DAG中入度爲零的點可能不止一個,因此拓撲序列並不必定是惟一的)
2.刪除與剛剛輸出的點有關的邊(即便那個點的出度爲零)
3.重複上述兩步,直至全部點都被輸出,或者再也不有入度爲零的點,後者說明這個圖是有環的,因此也能夠經過拓撲排序來判斷這個圖是否是有環圖
拓撲排序的代碼實現
有兩種方法來實現拓撲排序
1.Kahn算法
2.基於DFS實現
——Kahn算法——
Kahn算法的思路就是先使用一個棧用來保存入度爲零的點,而後輸出棧頂元素並將與棧頂元素有關的邊刪除,減小與棧頂元素有關的頂點的入度數量而且將入度減小到零的頂點也入棧。
具體的代碼實現以下(指針版):
1 bool Graph_DG::topological_sort(){ 2 cout << "圖的拓撲序列爲:" << endl; 3 //棧s用於保存棧爲空的頂點下標 4 stack<int> s; 5 int i; 6 ArcNode *temp; 7 //計算每一個頂點的入度,保存在indgree數組中 8 for (int i = 0; i != this -> vexnum; i++){ 9 temp = this -> arc[i].firstarc; 10 while (temp){ 11 ++this -> indegree[temp -> adjvex]; 12 temp = temp -> next; 13 } 14 } 15 //把入度爲0的頂點入棧 16 for (int i = 0; i != this -> vexnum; i++){ 17 if (!indegree[i]){ 18 s.push(i); 19 } 20 } 21 //count用於計算輸出的頂點個數 22 int count = 0; 23 while (!s.empty()){ 24 //若是棧爲空,則結束循環 25 i = s.top(); 26 s.pop(); 27 //保存棧頂元素,而且棧頂元素出棧 28 cout << this -> arc[i].data << " "; 29 temp = this -> arc[i].firstarc; 30 while (temp){ 31 if (!(--this -> indegree[temp -> adjvex])){ 32 //若是入度減小到爲0,則入棧 33 s.push(temp -> adjvex); 34 } 35 temp = temp -> next; 36 } 37 ++count; 38 } 39 if (count == this -> vexnum){ 40 couot << endl; 41 return true; 42 } 43 cout << "此圖有環,無拓撲序列" << endl; 44 return false;//說明這個圖有環 45 }
複雜度爲O(V + E)
——基於DFS實現——
推薦學習這個實現方式
由於這個比較好寫比較經常使用
一樣地,這個的複雜度也爲O(V + E)
藉助DFS來完成拓撲排序:
在訪問完一個結點以後把它加到當前拓撲序的首部
之因此不是尾部
是由於每次訪問完以後,當前的點都是最後的結點
好比1 -> 2, 2 -> 3
由於咱們在訪問的時候都是從前日後訪問的
因此最後訪問的數就是最後的數
具體的代碼實現以下(數組版):
1 int c[maxn]; 2 int topo[maxn], t; 3 bool dfs(int u){ 4 c[u] = -1;//表示正在訪問 5 for (int v = 0; v < n; v++) 6 if (G[u][v]){ 7 if (c[v] < 0) return false;//存在有向環,失敗退出 8 else if (!c[v] && !dfs(v)) return false; 9 } 10 c[u] = 1; 11 topo[--t] = u; 12 return true; 13 } 14 bool toposort(){ 15 t = n; 16 memset(c, 0, sizeof(c)); 17 for (int u = 0; u < n; u++) 18 if (!c[u]) 19 if (!dis(u)) return false; 20 return true; 21 }
這裏用到了一個c數組,c[u] = 0表示歷來沒有訪問過(歷來沒有調用過dfs(u)),c[u] = 1表示已經訪問過,而且還遞歸訪問過它的全部子孫(即dfs(u)曾被調用過,並已返回),c[u] = -1表示正在訪問(即遞歸調用dfs(u)正在棧幀中,還沒有返回
Tip:能夠用DFS求出有向無環圖(DAG)的拓撲排序。若是排序失敗,說明該有向圖存在有向環,不是DAG
有一道拓撲排序的模板題,超級裸
關於DP,由於這篇博客屬於DP專題中的一個小專題,因此在這裏不詳細講述DP了,只給出一些基礎的定義
DP(Dynamic Programming),中文名稱叫作動態規劃
動態規劃是一種用途很廣的問題求解方法,它自己並非一個特定的算法,而是一種思想,一種手段
動態規劃的理論性和實踐性都比較強,一方面須要理解「狀態」、「狀態轉移」、「最優子結構」、「重疊子問題」等概念
另外一方面又須要根據題目的條件靈活設計算法
因此說DP很是難玄啊
拓撲排序、DP和DAG上的動態規劃(拓撲排序 + DP)的關係
就相似於1 + 1 = 2的關係
因此在作完了準備工做後
我怎麼感受這是拓撲排序專題
開始進入正題
DAG上的動態規劃分爲兩個小題型
1.最長路及其字典序
2.固定終點的最長路和最短路
——最長路及其字典序——
模型:嵌套矩陣問題
有n個矩形,每一個矩形能夠用兩個整數a、b描述,表示它的長和寬。矩形X(a,b)能夠嵌套在矩形Y(c,d)中,當且僅當a < c,b < d,或者b < c,,a < d(至關於把矩形X旋轉90°)。例如,(1,5)能夠嵌套在(6,2)內,但不能嵌套在(3,4)內。你的任務是選出儘可能多的矩形排成一行,使得除了最後一個以外,每個矩形均可以嵌套在下一個矩形內。若是有多解,矩形編號的字典序應該儘可能小。
分析:
此題是一個很顯然的DAG上的動態規劃
矩陣之間的「可嵌套」是顯然的二元關係,二元關係能夠用圖來建模。若是矩陣X可嵌套在矩陣Y裏面,那麼就從X向Y連一條有向邊。這個有向圖是無環的,由於一個矩形沒法直接或者間接的嵌套在本身內部。
因此,這是一個DAG。這樣,咱們所求的就是DAG上的最長路徑
但咱們如何求DAG上不固定起點的最長路徑呢
仿照數字三角形的作法,咱們設d[i]爲從i點出發的最長路徑,由於第一步只能走到它的相鄰點,因此轉移方程很顯然的就能寫出
d(i) = max{d(j) + 1 | (i, j) ∈ E}
其中,E爲邊集,最終答案是全部d(i)中的最大值。
代碼以下:
1 int dp(int i){ 2 int &ans = d[i]; 3 if (ans > 0) return ans; 4 ans = 1; 5 for (int j = 1; j <= n; j++) 6 if (G[i][j]) ans = max(ans, dp(j) + 1); 7 return ans; 8 }
原題中還有一個要求,就是要求字典序最小,咱們只須要