动态规划理论学习
文章目錄
- 1. 理論總結
- 1.1 “一個模型”
- 1.2 “三個特征”
- 1.2.1 最優子結構
- 1.2.2 無后效性
- 1.2.3 重復子問題
- 2. 實例剖析
- 2.1 問題描述
- 2.2 兩種DP解題思路
- 2.2.1 狀態轉移表
- 2.2.2 狀態轉移方程
- 3. 四種算法思想比較
1. 理論總結
動態規劃理論總結為“一個模型、三個特征”。
1.1 “一個模型”
- 它指的是動態規劃適合解決的問題的模型。我把這個模型定義為“多階段決策最優解模型"。
- 一般是用動態規劃來解決最優問題。
- 解決問題的過程,需要經歷多個決策階段。每個決策階段對應著一組狀態。
- 然后我們尋找一組決策序列,經過這組決策序列,能夠產生最終期望求解的最優值。
1.2 “三個特征”
1.2.1 最優子結構
- 問題的最優解包含子問題的最優解。
- 反過來說就是,可以通過子問題的最優解,推導出問題的最優解。后面階段的狀態可以通過前面階段的狀態推導出來。
1.2.2 無后效性
- 在推導后面階段的狀態時,我們只關心前面階段的狀態值,不關心這個狀態是怎么一步一步推導出來的。
- 某階段狀態一旦確定,就不受之后階段的決策影響。只要滿足前面提到的動態規劃問題模型,其實基本上都會滿足無后效性。
1.2.3 重復子問題
- 不同的決策序列,到達某個相同的階段時,可能會產生重復的狀態。
2. 實例剖析
2.1 問題描述
一個n乘以n的矩陣w[n][n]。存儲的都是正整數。棋子起始位置在左上角,終止位置在右下角。每次只能向右或者向下移動一位。把每條路徑經過的數字加起來看作路徑的長度。最短路徑長度是多少?
- 是否符合“一個模型”
- 是否符合“三個特征”
我們可以用回溯算法來解決這個問題。自己寫一下代碼,畫一下遞歸樹,就會發現,遞歸樹中有重復的節點。重復的節點表示,從左上角到節點對應的位置,有多種路線,這也能說明這個問題中存在重復子問題。
下面給出回溯解法
/*** @description: dp課第二節,案例回溯法求解* @author: michael ming* @date: 2019/7/19 19:55* @modified by: */ #include <iostream> #define N 4//地圖大小 #define k (2*N-1)//需要走的步數 using namespace std; int selectWay[k], shortestWay[k]; void step(int (*map)[N], int s, int &mins, int r, int c, int idx) {selectWay[idx++] = map[r][c];//記錄選擇的路if(r == N-1 && c == N-1){if(s < mins){mins = s;//更新最小的總路程for(int i = 0; i < k; ++i)//把最終的路線記錄下來shortestWay[i] = selectWay[i];}return;}if(r == N || c == N)return;//走出地圖邊界了step(map,s+map[r+1][c],mins,r+1,c,idx);//往下走step(map,s+map[r][c+1],mins,r,c+1,idx);//往右走 } int main() {int s = 0, mins = 65535;int map[N][N] = {1,3,5,9,2,1,3,4,5,2,6,7,6,8,4,3};step(map,s+map[0][0],mins,0,0,0);cout << "最短路徑是:" << mins << endl;cout << "走過的點的距離分別是:" << endl;for(int i = 0; i < k; ++i)cout << shortestWay[i] << " ";return 0; }走到(i,j)這個位置,只能通過(i-1,j),(i,j-1)這兩個位置移動過來,也就是,想要計算(i,j)位置對應的狀態,只需關心(i-1,j),(i,j-1)兩個位置對應的狀態,并不關心棋子是通過什么樣的路線到達這兩個位置。而且,我們僅僅允許往下和往右移動,不允許后退,所以,前面階段的狀態確定后,不會被后面的決策所改變,所以,這個問題符合“無后效性”這一特征。
把從起始位置(0,0)到(i,j)的最小路徑,記作函數min_dist(i,j)。因為只能往右或往下移動,所以只有可能從(i,j-1)或(i-1,j)兩個位置到達(i,j)。到達(i,j)的最短路徑肯定包含到達這兩個位置的最短路徑之一。換句話說就是,min_dist(i,j)可以通過min_dist(i,j-1)和min_dist(i-1,j)兩個狀態推導出來。這就說明,這個問題符合“最優子結構”。
min_dist(i, j) = w[i][j] + min{min_dist(i, j-1), min_dist(i-1, j)}2.2 兩種DP解題思路
2.2.1 狀態轉移表
-
一般能用動態規劃的,都可以使用回溯暴力搜索。所以,可以先用簡單的回溯算法解決,然后定義狀態,對應畫出遞歸樹。
-
從遞歸樹中,我們很容易可以看出來,是否存在重復子問題,以及重復子問題是如何產生的。以此來尋找規律,看是否能用動態規劃解決。
-
找到重復子問題之后,有兩種處理思路,第一種是回溯加“備忘錄”的方法,來避免重復子問題。從效率上來講,這跟動態規劃的解決思路沒有差別。
-
第二種是使用動態規劃,狀態轉移表法。
-
先畫出一個狀態表,一般是二維的,可以把它想象成二維數組。其中,每個狀態包含三個變量,行、列、數組值。
-
根據決策的先后,從前往后,根據遞推關系,分階段填充狀態表中的每個狀態。最后,將這個遞推填表的過程,翻譯成代碼,就是動態規劃代碼。
-
盡管大部分狀態表都是二維的,如果問題的狀態比較復雜,需要很多變量來表示,那對應的狀態表就是高維的,這個時候,不適合用狀態轉移表法來解決了。一方面高維狀態轉移表不好畫圖表示,另一方面人腦不擅長思考高維的東西。
根據回溯代碼畫出遞歸樹,遞歸樹中,一個狀態(節點)包含三個變量(i,j,dist),其中i,j表示行和列,dist表示從起點到達點(i,j)的路徑長度。圖中看出,盡管(i,j,dist)不存在重復,但是(i,j)重復的有很多。對(i,j)重復的節點,我們只選擇 dist最小的節點,繼續遞歸求解,其他節點舍棄。
畫出二維狀態表,表中行、列表示棋子位置,表中數值表示從起點到這個位置的最短路徑。我們按照決策過程,將狀態表填好。為了方便,我們按行來進行依次填充。
dp狀態表法代碼如下:
/*** @description: * @author: michael ming* @date: 2019/7/19 23:30* @modified by: */ #include <iostream> #include <stack> #define N 4//地圖大小 using namespace std; void printShortestWay(int (*map)[N], int (*states)[N]) {stack<int> path;path.push(map[N-1][N-1]);//終點for(int i = N-1,j = N-1; j != 0 && i != 0; ){if(states[i][j]-map[i][j] == states[i-1][j])path.push(map[--i][j]);//從上面過來的elsepath.push(map[i][--j]);//從左邊過來的}path.push(map[0][0]);//起點cout << "走過的點的距離分別是:" << endl;while(!path.empty())//棧逆序彈出路徑{cout << path.top() << " ";path.pop();} } void step_dp(int (*map)[N]) {int (*states)[N] = new int [N][N];int i, j, sum = 0;for(j = 0; j < N; ++j)//初始化第一行狀態{sum += map[0][j];states[0][j] = sum;}sum = 0;for(i = 0; i < N; ++i)//初始化第一列狀態{sum += map[i][0];states[i][0] = sum;}for(i = 1; i < N; ++i)//填寫狀態表for(j = 1; j < N; ++j)states[i][j] = map[i][j]+min(states[i][j-1],states[i-1][j]);cout << "最短路徑是:" << states[N-1][N-1] << endl;printShortestWay(map,states);delete [] states;return; } int main() {int map[N][N] = {1,3,5,9,2,1,3,4,5,2,6,7,6,8,4,3};step_dp(map);return 0; }2.2.2 狀態轉移方程
-
狀態轉移方程法有點類似遞歸。根據最優子結構,寫出遞歸公式,也就是狀態轉移方程。
-
有兩種代碼實現方法,一種是遞歸加“備忘錄”,另一種是迭代遞推。
min_dist(i, j) = w[i][j] + min{min_dist(i, j-1), min_dist(i-1, j)} -
狀態轉移方程是解DP的關鍵。如果能寫出狀態轉移方程,那DP問題基本上就解決一大半了。但是很多DP問題的狀態本身就不好定義,狀態轉移方程也就更不好想到。
下面用遞歸加“備忘錄”的方式,將狀態轉移方程翻譯成代碼。對于另一種實現方式,跟狀態轉移表法的代碼實現是一樣的,只是思路不同。
/*** @description: dp 狀態方程 遞歸* @author: michael ming* @date: 2019/7/20 9:35* @modified by: */ #include <iostream> #include <stack> #define N 4//地圖大小 using namespace std; int states [N][N]; void printShortestWay(int (*map)[N]) {stack<int> path;path.push(map[N-1][N-1]);//終點for(int i = N-1,j = N-1; j != 0 && i != 0; ){if(states[i][j]-map[i][j] == states[i-1][j])path.push(map[--i][j]);//從上面過來的elsepath.push(map[i][--j]);//從左邊過來的}path.push(map[0][0]);//起點cout << "走過的點的距離分別是:" << endl;while(!path.empty())//棧逆序彈出路徑{cout << path.top() << " ";path.pop();} } int minDist(int (*map)[N], int i, int j)//從起點到i,j點的最小距離 {if(i == 0 && j == 0)//從起點到起點,返回該位置數值return map[0][0];if(states[i][j] > 0)//遇到算過的,直接返回結果return states[i][j];int minLeft, minUp;minLeft = minUp = 65535;if(j-1 >= 0)minLeft = minDist(map,i,j-1);//點左邊的點的最小距離if(i-1 >= 0)minUp = minDist(map,i-1,j);//點上面的點的最小距離int currMinDist = map[i][j]+min(minLeft,minUp);states[i][j] = currMinDist;//備忘錄更新return currMinDist; } int main() {int map[N][N] = {1,3,5,9,2,1,3,4,5,2,6,7,6,8,4,3};cout << "最短路徑是:" << minDist(map,N-1,N-1) << endl;printShortestWay(map);return 0; }
強調一點,不是每個問題都同時適合這兩種解題思路。有的問題可能用狀態表更清晰,而有的問題可能用狀態方程思路更清晰。
3. 四種算法思想比較
到現在為止,已經學習了四種算法思想,貪心、分治、回溯、動態規劃。
- 貪心、回溯、動態規劃,都可以抽象成多階段決策最優解模型
- 而分治解決的問題盡管大部分也是最優解問題,但是,大部分都不能抽象成多階段決策模型
| 回溯 | 窮舉所有的情況,然后對比得到最優解。時間復雜度非常高,指數級,只能用來解決小規模問題。大規模問題,執行效率很低 |
| 動態規劃 | 需要滿足三個特征,最優子結構、無后效性和重復子問題,動態規劃之所以高效,是因為回溯算法實現中存在大量的重復子問題 |
| 分治 | 要求分割成的子問題,不能有重復子問題,與動態規劃正好相反 |
| 貪心 | 高效,代碼簡潔。可以解決的問題也有限。需要滿足三個條件,最優子結構、無后效性和貪心選擇性。“貪心選擇性”的意思是,通過局部最優的選擇,能產生全局的最優選擇。每一個階段,都選擇當前看起來最優的決策,所有階段的決策完成之后,最終由這些局部最優解構成全局最優解 |
總結
- 上一篇: txt文件可存储最大值_Verilog边
- 下一篇: 漂亮的html页面源码_爬虫数据分析之h