動態規劃算法原理與實踐

動態規劃(Dynamic Programming)是解決多階段決策問題經常使用的最優化理論,動態規劃和分治法同樣,也是經過定義子問題,先求解子問題,而後在由子問題的解組合出原問題的解。可是它們之間的不一樣點是分治法的子問題之間是相互獨立的,而動態規劃的子問題之間存在堆疊關係(遞推關係式肯定的遞推關係)。動態規劃方法的原理就是把多階段決策過程轉化爲一系列的單階段決策問題,利用各個階段之間的遞推關係,逐個肯定每一個階段的最優化決策,最終堆疊出多階段決策的最優化決策結果。動態規劃問題有不少模型,常見的有線性模型、(字符或數字)串模型、區間模型、狀態壓縮模型,等等,本節課後面介紹的最長公共子序列問題,就是一個典型的串模型。css

動態規劃比窮舉高效,這一點在不少狀況下都獲得了印證,這經常給人一種錯覺,覺得它是高效的多項式時間算法,可是事實並不是如此。動態規劃法對全部子問題求解的內在機制實際上是一種廣域搜索,其效率在很大程度上仍是取決於問題自己。每種方法都有自身的侷限性,動態規劃法也不是萬能的。動態規劃適合求解多階段(狀態轉換)決策問題的最優解,也可用於含有線性或非線性遞推關係的最優解問題,可是這些問題都必須知足最優化原理和子問題的「無後向性」。html

  • 最優化原理:最優化原理其實就是問題的最優子結構的性質,若是一個問題的最優子結構是不論過去狀態和決策如何,對前面的決策所造成的狀態而言,其後的決策必須構成最優策略。也就是說,無論以前的決策是不是最優決策,都必須保證從如今開始的決策是在以前決策基礎上的最優決策,則這樣的最優子結構就符合最優化原理。算法

  • 無後向性(無後效性):所謂「無後向性」,就是當各個階段的子問題肯定之後,對於某個特定階段的子問題來講,它以前各個階段的子問題的決策隻影響該階段的決策,對該階段以後的決策不產生影響。數組

這裏須要解釋一下無後向性。在解釋以前,咱們先淡化一下階段的概念,只強調狀態(決策狀態),事實上,這是我本人學動態規劃法過程當中的一點經驗。多階段決策過程當中,隨着子問題的劃分會產生不少狀態,對於某一個狀態 S 來講,只要 S 狀態肯定了之後,S 之後的那些依靠 S 狀態作最優選擇的狀態也就都肯定了,S 以後的狀態只受 S 狀態的影響。也就是說,不管以前是通過何種決策途徑來到了 S 狀態,S 狀態肯定之後,其後續狀態的演化結果都是同樣的,不會由於到達 S 狀態的決策路徑的不一樣而產生不一樣的結果,這就是無後向性。dom

動態規劃的基本思想

和分治法同樣,動態規劃解決複雜問題的思路也是先對問題進行分解,而後經過求解小規模的子問題再反推出原問題的結果。可是動態規劃分解子問題不是簡單地按照「大事化小」的方式進行的,而是沿着決策的階段來劃分子問題,決策的階段能夠隨時間劃分,也能夠隨着問題的演化狀態來劃分。分治法要求子問題是互相獨立的,以便分別求解並最終合併出原始問題的解。分治法對全部的子問題都「一視同仁」地進行計算求解,若是分解的子問題中存在相同子問題,就會存在重複求解子問題的狀況。優化

好比某個問題 A,第一次分解爲 A1 和 A2 兩個子問題,A1 又可分解爲 A11 和 A12 兩個子問題,A2 又分解爲 A21 和 A22 兩個子問題,分治法會分別求解 A十一、A十二、A21 和 A22 四個子問題,即使 A12 和 A21 是相同的子問題,分治法也依然會計算四次子問題的解,這就存在重複計算的問題,重複計算相同的子問題會影響求解的效率。spa

與之相反,動態規劃法的子問題不是互相獨立的,子問題之間一般有包含關係,甚至兩個子問題能夠包含相同的子子問題。好比,子問題 A 的解可能由子問題 C 的解遞推獲得,同時,子問題 B 的解也可能由子問題 C 的解遞推獲得。對於這種狀況,動態規劃法對子問題 C 只求解一次,而後將其結果保存在一張表中(此表也被稱爲備忘錄)。當求解子問題 A 或子問題 B 的時候,若是發現子問題 C 已經求解過(在備忘錄表中能查到),則再也不求解子問題 C,而是直接使用從表中查到的子問題 C 的解,避免了子問題 C 被重複計算求解的問題。.net

動態規劃法不像貪婪法或分治法那樣有固定的算法實現模式,線性規劃問題和區間動態規劃問題的實現方法就徹底不一樣。做爲解決多階段決策最優化問題的一種思想,能夠用帶備忘錄的窮舉方法實現,也能夠根據堆疊子問題之間的遞推公式用遞推的方法實現。可是從算法設計的角度分析,使用動態規劃法通常須要四個步驟,分別是:設計

  1. 定義最優子問題(最優解的子結構)指針

  2. 定義狀態(最優解的值)

  3. 定義決策和狀態轉換方程(定義計算最優解的值的方法)

  4. 肯定邊界條件

這四個問題解決了,算法也就肯定了。接下來就結合兩個實例分別介紹這四個步驟,這兩個例子分別是經典的 0-1 揹包問題和最長公共子序列問題。

定義最優子問題

定義最優子問題,也就是最優解的子結構,它肯定問題的優化目標以及如何決策最優解,並對決策過程劃分階段。所謂階段,能夠理解爲一個問題從開始到解決須要通過的環節,這些環節先後關聯。

劃分階段沒有固定的方法,根據問題的結構,能夠是按照時間或動做的順序劃分階段,好比《算法導論》書中介紹的「裝配線與工做站問題「;也能夠是按照問題的組合狀態劃分階段,好比經典的「凸多邊形三角剖分問題」。階段劃分之後,對問題的求解就變成了各個階段分別進行最優化決策,問題的解就變成按照階段順序依次選擇的一個決策序列。

對於 0-1 揹包問題,每選擇裝一個物品能夠看作是一個階段,其子問題就能夠定義爲每次向包中裝(選擇)一個物品,直到超過揹包的最大容量爲止。最長公共子序列問題能夠按照問題的演化狀態劃分階段,這就須要先定義狀態,有了狀態的定義,只要狀態發生了變化,就能夠認爲是一個階段。

定義狀態

狀態既是決策的對象,也是決策的結果,對於每一個階段來講,對起始狀態施加決策,使得狀態發生改變,獲得決策的結果狀態。初始狀態通過每個階段的決策(狀態改變)以後,最終獲得的狀態就是問題的解。固然,不是全部的決策序列施加於初始狀態後均可以獲得最優解,只有一個決策序列能獲得最優解。狀態的定義是創建在子問題定義基礎之上的,所以狀態必須知足無後向性要求。必要時,能夠增長狀態的維度,引入更多的約束條件,使得狀態定義知足無後向性要求。

0-1 揹包問題自己是個線性過程,可是若是簡單將狀態定義爲裝入的物品編號,也就是定義 s[i] 爲裝入第 i 件物品後得到的最大價值,則子問題沒法知足無後向性要求,緣由是以前的任何一個決策都會影響到全部的後序決策(由於裝入物品後背包容量發生變化了),所以須要增長一個維度的約束。

考慮到每裝入一個物品,揹包的剩餘容量就會減小,故而選擇將揹包容量也包含在狀態定義中。最終揹包問題的狀態 s[i,j] 定義爲將第 i 件物品裝入容量爲 j 的揹包中所能得到的最大價值。對於最長公共子序列問題,若是定義 str1[1…i] 爲第一個字符串前 i 個字符組成的子串,定義 str2[1…j] 爲第二個字符串的前 j 個字符組成的子串,則最長公共子序列問題的狀態 s[i,j] 定義爲 str1[1…i] 與 str2[1…j] 的最長公共子序列長度。

定義決策和狀態轉換方程

決策就是能使狀態發生轉變的選擇動做,若是選擇動做有多個,則決策就是取其中能使得階段結果最優的那一個。狀態轉換方程是描述狀態轉換關係的一系列等式,也就是從 n-1 階段到 n 階段演化的規律。狀態轉換取決於子問題的堆疊方式,若是狀態定義得不合適,會致使子問題之間沒有重疊,也就不存在狀態轉換關係了。沒有狀態轉換關係,動態規劃也就沒有意義了,實際算法就退化爲像分治法那樣的樸素遞歸搜索算法了。

0-1 揹包問題的決策很簡單,那就是決定是否選擇第 i 件物品,即判斷裝入第 i 件物品得到的收益最大仍是不裝入第 i 件物品得到的收益最大。若是不裝入第 i 件物品,則揹包內物品的價值仍然是 s[i-1,j] 狀態,若是裝入第 i 件物品,則揹包內物品的價值就變成了 s[i,j-Vi] + Pi 狀態,其中 Vi 和 Pi 分別是第 i 件物品的容積和價值,決策的狀態轉換方程就是:

s[i,j]=max(s[i−1,j],s[i,j−Vi]+Pi) 

最長公共子序列問題的決策方式就是判斷 str1[i] 和 str2[j] 的關係,若是 str1[i] 與 str2[j] 相同,則公共子序列的長度應該是 s[i-1,j-1] + 1,不然就分別嘗試匹配 str1[1…i+1] 與 str2[1…j] 的最長公共子序列,以及 str1[1…i] 與 str2[1…j+1] 的最長公共子序列,而後取兩者中較大的那個值做爲 s[i,j] 的值。最長公共子序列問題的狀態轉換方程就是:

s[i,j]=s[i−1,j−1]+1 

其中,str1[i] 與 str2[j] 相同。

s[i,j]=max(s[i,j−1],s[i−1,j])

其中,str1[i] 與 str2[j] 不相同。

肯定邊界條件

對於遞歸加備忘錄方式(記憶搜索)實現的動態規劃方法,邊界條件實際上就是遞歸終結條件,無需額外的計算。對於使用遞推關係直接實現的動態規劃方法,須要肯定狀態轉換方程的遞推式的初始條件或邊界條件,不然沒法開始遞推計算。

0-1 揹包問題的邊界條件很簡單,就是沒有裝入任何物品的狀態:

s[0,Vmax]=0 

若要肯定最長公共子序列問題的邊界條件,要從其決策方式入手,當兩個字符串中的一個長度爲 0 時,其公共子序列長度確定是 0,所以其邊界條件就是:

s[i,j]=0 

其中,i=0 或 j=0。 

動態問題分類

對問題進行分類,主要有如下幾種:

達到目標的最小(最大)路徑

問題列表: https://leetcode.com/list/55ac4kuc

聲明

給定目標,找到達到目標的最小(最大)成本/路徑/總和。

方法

在當前狀態以前的全部可能路徑中選擇最小(最大)路徑,而後爲當前狀態添加值。

routes[i] = min(routes[i-1], routes[i-2], ... , routes[i-k]) + cost[i]

爲目標中的全部值生成最佳解決方案,而後返回目標的值。

for (int i = 1; i <= target; ++i) {
   for (int j = 0; j < ways.size(); ++j) {
       if (ways[j] <= i) {
           dp[i] = min(dp[i], dp[i - ways[j]] + cost / path / sum) ;
       }
   }
}
 
return dp[target]

相似問題

746.最低價攀登樓梯 Easy 

for (int i = 2; i <= n; ++i) {
   dp[i] = min(dp[i-1], dp[i-2]) + (i == n ? 0 : cost[i]);
}
 
return dp[n]

64.最小路徑總和 Medium

for (int i = 1; i < n; ++i) {
   for (int j = 1; j < m; ++j) {
       grid[i][j] = min(grid[i-1][j], grid[i][j-1]) + grid[i][j];
   }
}
 
return grid[n-1][m-1]

322.硬幣找零 Medium

for (int j = 1; j <= amount; ++j) {
   for (int i = 0; i < coins.size(); ++i) {
       if (coins[i] <= j) {
           dp[j] = min(dp[j], dp[j - coins[i]] + 1);
       }
   }
}

931.最小降低路徑總和 Medium

983.最低票價 Medium

650. 2鍵鍵盤 Medium

279.完美正方形 Medium

1049.最後一塊石頭的重量II Medium

120.三角形 Medium

474.一和零 Medium

221.最大廣場 Medium

322.硬幣找零 Medium

1240.用最小的正方形平鋪一個矩形 Hard

174.地下城遊戲 Hard

871.最小加油站數 Hard

 

不一樣路徑

問題列表:Problem List: https://leetcode.com/list/55ajm50i

聲明

給定目標,能夠找到許多不一樣的路勁到達目標​​。 

方法

總結全部可能的方法以達到當前狀態。

routes[i] = routes[i-1] + routes[i-2], ... , + routes[i-k] 

爲目標中的全部值生成總和,而後返回目標的值。

for (int i = 1; i <= target; ++i) {
   for (int j = 0; j < ways.size(); ++j) {
       if (ways[j] <= i) {
           dp[i] += dp[i - ways[j]];
       }
   }
}
 
return dp[target]

相似問題

70.爬樓梯 easy

for (int stair = 2; stair <= n; ++stair) {
   for (int step = 1; step <= 2; ++step) {
       dp[stair] += dp[stair-step];   
   }
}

62.獨特的道路 Medium

for (int i = 1; i < m; ++i) {
   for (int j = 1; j < n; ++j) {
       dp[i][j] = dp[i][j-1] + dp[i-1][j];
   }
}

1155.目標總數的骰子卷數 Medium

for (int rep = 1; rep <= d; ++rep) {
   vector<int> new_ways(target+1);
   for (int already = 0; already <= target; ++already) {
       for (int pipe = 1; pipe <= f; ++pipe) {
           if (already - pipe >= 0) {
               new_ways[already] += ways[already - pipe];
               new_ways[already] %= mod;
           }
       }
   }
   ways = new_ways;
} 

PS: 一些問題指出了重複的次數,在這種狀況下,還要增長一個循環來模擬每一個重複。

688.國際象棋騎士的機率 Medium

494.目標總和 Medium

377.組合和IV Medium

935.騎士撥號器 Medium

1223.骰子滾動模擬 Medium

416.分區相等子集總和 Medium

808.湯服務 Medium

790. Domino和Tromino平鋪 Medium 

801.使序列增長的最小掉期

673.最長遞增子序列數 Medium

63.獨特之路II Medium

576.超越界限 Medium

1269.通過一些步驟後留在同一個地方的方式數量 Hard

1220.元音排列 Hard

合併問題

問題列表: https://leetcode.com/list/55aj8s16


此類問題的陳述模式以下:

給定一組數字,考慮到當前數字以及從左側和右側可得到的最佳數值,找到解決問題的最佳方案。

方法

找到每一個間隔的全部最佳解決方案,並返回最佳答案。 

// from i to j dp[i][j] = dp[i][k] + result[k] + dp[k+1][j]

從左側和右側得到最佳效果,併爲當前位置添加解決方案。

for(int l = 1; l<n; l++) {
   for(int i = 0; i<n-l; i++) {
       int j = i+l;
       for(int k = i; k<j; k++) {
           dp[i][j] = max(dp[i][j], dp[i][k] + result[k] + dp[k+1][j]);
       }
   }
}
 
return dp[0][n-1]

相似問題

1130.從葉值得出的最小成本樹 Medium

for (int l = 1; l < n; ++l) {
   for (int i = 0; i < n - l; ++i) {
       int j = i + l;
       dp[i][j] = INT_MAX;
       for (int k = i; k < j; ++k) {
           dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + maxs[i][k] * maxs[k+1][j]);
       }
   }
}

96.惟一二進制搜索樹 Medium

1039.多邊形的最小分數三角剖分 Medium

546.刪除盒子 Medium

1000.合併石頭的最低成本 Medium

312.爆裂氣球 Hard

375.猜猜數字更高或更低II Medium

 

字符串上的dp

此模式的通常問題陳述可能會有所不一樣,但大多數狀況下會給您兩個字符串,而這些字符串的長度並不大 

問題描述

給定兩個字符串 s1 和 s2,返回某種結果。 

方法

這種模式中的大多數問題都須要一個能夠接受O(n^2)複雜度的解決方案。

// i - indexing string s1
// j - indexing string s2
for (int i = 1; i <= n; ++i) {
   for (int j = 1; j <= m; ++j) {
       if (s1[i-1] == s2[j-1]) {
           dp[i][j] = /*code*/;
       } else {
           dp[i][j] = /*code*/;
       }
   }
}

若是給你一個字符串s,方法可能幾乎沒有變化

for (int l = 1; l < n; ++l) {
   for (int i = 0; i < n-l; ++i) {
       int j = i + l;
       if (s[i] == s[j]) {
           dp[i][j] = /*code*/;
       } else {
           dp[i][j] = /*code*/;
       }
   }
} 

1143.最長公共子序列 Medium

for (int i = 1; i <= n; ++i) {
   for (int j = 1; j <= m; ++j) {
       if (text1[i-1] == text2[j-1]) {
           dp[i][j] = dp[i-1][j-1] + 1;
       } else {
           dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
       }
   }
}

647.迴文子串 Medium

for (int l = 1; l < n; ++l) {
   for (int i = 0; i < n-l; ++i) {
       int j = i + l;
       if (s[i] == s[j] && dp[i+1][j-1] == j-i-1) {
           dp[i][j] = dp[i+1][j-1] + 2;
       } else {
           dp[i][j] = 0;
       }
   }
}

516.最長迴文序列 Medium

1092.最短的公共超序列 Medium

72.編輯距離 Hard

115.不一樣的子序列 Hard

712.兩個字符串的最小ASCII刪除總和 Medium

5.最長迴文子串 Medium

作決定


 問題列表:https://leetcode.com/list/55af7bu7

對於這種模式的通常問題陳述是在該狀況下是否決定使用當前狀態。所以,問題須要您在當前狀態下作出決定。

聲明

給定一組值,找到答案,並提供選擇或忽略當前值的選項。

方法

若是決定選擇當前值,請使用先前的結果而且須要考慮當前的狀態;反之亦然,若是您決定忽略當前值,請使用使用值的先前結果。

// i - indexing a set of values
// j - options to ignore j values
for (int i = 1; i < n; ++i) {
   for (int j = 1; j <= k; ++j) {
       dp[i][j] = max({dp[i][j], dp[i-1][j] + arr[i], dp[i-1][j-1]});
       dp[i][j-1] = max({dp[i][j-1], dp[i-1][j-1] + arr[i], arr[i]});
   }
}

198.強盜屋 Easy

for (int i = 1; i < n; ++i) {
   dp[i][1] = max(dp[i-1][0] + nums[i], dp[i-1][1]);
   dp[i][0] = dp[i-1][1];
} 

121.買賣股票的最佳時間 Easy

714.帶有交易費的最佳買賣股票時間 Medium

309.有冷卻時間買賣股票的最佳時機 Medium

123.最佳買賣股票時間 Hard

188.最佳買賣股票時間IV Hard

總結

一、動態規劃其實就是窮舉遍歷,並從中找出一個最優的解。所以,各類最短,最長問題均可以考慮動態規劃。動態規劃的窮舉有點特別,由於這類問題存在「重疊⼦問題」,若是

暴⼒窮舉的話效率會極其低下,因此須要「備忘錄」或者「DP table」來優化窮舉過程,避免沒必要要的計算。

二、雖然動態規劃的核⼼思想就是窮舉求最值,可是問題能夠變幻無窮,窮舉全部可⾏解其實並非⼀件容易的事,只有列出正確的「狀態轉移⽅程」才能正確地窮舉。

三、對於狀態轉移方程,不少時候咱們不知道 dp 是幾維數組,其實只要看題目有多少變量,有幾個變量就是幾維,結果是要求的解。其實就是 f(x,y,z)=m; x, y, z, 和 m 的含義都理清楚了,這個狀態方程就對了。

 

算法系列文章

滑動窗口算法基本原理與實踐

二分查找法基本原理和實踐

廣度優先搜索原理與實踐

深度優先搜索原理與實踐

雙指針算法基本原理和實踐

分治算法基本原理和實踐

動態規劃算法原理與實踐

算法筆記

 

參考文章

爲何你學不過動態規劃?告別動態規劃,談談個人經驗

相關文章
相關標籤/搜索