一、什么是動態規划
動態規划(DP)是一種用來解決一類最優化問題的算法思想。簡單來說,動態規划將一個復雜的問題分解成若干個子問題,通過綜合子問題的最優解來得到原問題的最優解。
二、動態規划的遞歸寫法
以斐波那契(Fibonacci) 數列為例,斐波那契數列的定義為 F0=1,F1=1,Fn=Fn-1+Fn-2 (n≥2)。為了避免重復計算,可以開一個一維數組 dp,用以保存已經計算過的結果。代碼如下:
1 int dp[maxn]; 2 // 斐波那契數列遞歸寫法 3 int F(int n) { 4 if(n == 0 || n==1) return 1; // 遞歸邊界 5 if(dp[n] != -1) return dp[n]; // 已經計算過 6 else { 7 dp[n] = F(n-1) + F(n-2); // 計算F(n),並保存 8 return dp[n]; // 返回 F(n) 結果 9 } 10 }
三、 動態規划的遞歸寫法
以經典的數塔問題為例,如下圖所示,將一些數字排成數塔的形狀,其中第一層有一個數字,第二層有兩個數字……第 n 層有 n 個數字。現在要從第一層走到第 n 層,每次只能走向下一層連接的兩個數字中的一個,問:最后將路徑上所有數字相加后得到的和最大是多少?
不妨令 dp[i][j] 表示從第 i 行第 j 個數字出發到達最底層的所有路徑中能得到的最大和,在定義了這個數組后,dp[1][1] 就是最終想要的答案。
如果想求出 dp[i][j],那么一定要先求出它的兩個子問題“從位置 (i+1,j) 到達最底層的最大和 dp[i+1][j]”和“從位置 (i+1,j+1) 到達最底層的最大和 dp[i+1][j+1]”,即進行了一次決策:走左下還是右下。寫成式子就是:
dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j]
其中 f[i][j] 存放第 i 層的第 j 個數字。代碼如下:
1 /* 2 動態規划的遞推寫法 3 */ 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <math.h> 8 #include <stdlib.h> 9 #include <time.h> 10 #include <stdbool.h> 11 12 #define maxn 1000 13 int f[maxn][maxn], dp[maxn][maxn]; 14 15 // 較大值 16 int max(int a, int b) { 17 return a>b ? a : b; 18 } 19 20 int main() { 21 int n; 22 scanf("%d", &n); 23 int i, j; 24 for(i=1; i<=n; ++i) { 25 for(j=1; j<=i; ++j) { 26 scanf("%d", &f[i][j]); // 輸入數塔 27 } 28 } 29 // 邊界 30 for(j=1; j<=n; ++j) { 31 dp[n][j] = f[n][j]; 32 } 33 // 從第 n-1 層往上計算 dp[i][j] 34 for(i=n-1; i>=1; --i) { 35 for(j=1; j<=i; ++j) { 36 dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j]; 37 } 38 } 39 printf("%d\n", dp[1][1]); // dp[1][1] 即為需要的答案 40 41 return 0; 42 }
下面指出兩對概念的區別:
1. 分治與動態規划。分治和動態規划都是將問題分解為子問題,然后合並子問題的解得到原問題的解。但是不同的是,分治法分解出的子問題是不重疊的,因此分治法解決的問題不擁有重疊子問題,而動態規划解決的問題擁有重疊子問題。另外,分治法解決的問題不一定是最優化問題,而動態規划解決的問題一定是最優化問題。
2. 貪心與動態規划。貪心和動態規划都要求原問題必須擁有最優子結構。二者的區別在於,貪心法通過一種策略直接選擇一個子問題去求解,沒被選擇的子問題就不去求解了,直接拋棄。也就是說,它總是只在上一步選擇的基礎上繼續選擇,因此整個過程以一種單鏈的流水方式進行。而動態規划總是從邊界開始向上得到目標問題的解。也就是說,它總是會考慮所有子問題,並選擇繼承能得到最優結果的那個,對暫時沒被繼承的子問題,由於重疊子問題的存在,后期可能會再次考慮它們,因此還有機會成為全局最優的一部分,不需要放棄。