1. 程式人生 > >動態規劃-遞迴與遞推寫法

動態規劃-遞迴與遞推寫法

# 動態規劃 [TOC] 動態規劃(Dynamic Programming, DP)是一種用來解決一類**最優化問題**的演算法思想。 簡單來說,動態規劃是將一個複雜的問題分解成若干個子問題,通過綜合子問題的最優解來得到原問題的最優解。動態規劃會把每個求解過的子問題的解記錄下來,這樣當下一次碰到同樣的子問題時,就可以直接使用之前記錄的結果,而不是重複計算。 一般可以使用遞迴或者遞推的方法的寫法來實現動態規劃,其中遞迴寫法又稱為 **記憶化搜尋**。 ## 遞迴寫法(自頂向下) 動態規劃記錄子問題,避免下次遇到相同的子問題時重複計算。 以斐波那契數列為例,F0 = 1,F1 = 2,Fn = Fn-1 + Fn-2 。 遞迴寫法 ```cpp int F(int n) { if(n == 0 || n == 1) return 1; else return F(n-1) + F(n-2); } ``` 事實上這個遞迴會重複計算很多次,例如F(5) = F(4) + F(3), F(4) = F(3) + F(2)。這個時候如果不採取措施,F(3) 會被計算兩次,如果 n 很大,重複計算的次數很大, 為了**避免重複計算**,可以開一個一維陣列 dp[],用以儲存已經計算過的結果,其中 dp[n] 記錄 F(n) 的結果,並用 dp[n] = -1 表示還沒計算過。 ```cpp int dp[maxn]; int F(int n) { if(n == 0 || n == 1) return 1; if(dp[n] != -1) return dp[n]; else { dp[n] = F(n-1) + F(n-2); // 計算 F(n),並且儲存在 dp[n] 中 return dp[n]; } } ``` ## 遞推寫法(自底向上) 還是以我們剛才的求解斐波那契數列為例,遞推寫法就應該從底部向上求解 ```cpp int dp[maxn]; int F(int n) { dp[0] = dp[1] = 1; for(int i = 2; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; } ``` 遞推寫法也就是從子問題開始求解,一直向上求解直到我們要求解的問題被解決。 ## 核心思想 如果一個問題的最優解可以由其子問題的最優解有效的構造出來,那麼稱這個問題擁有 **最優子結構(Optimal Substructure)**。最優子結構保證了動態規劃中原問題的最優解可以由子問題的最優解推導而來。 因此,一個問題必須擁有**最優子結構**和**重疊子問題**才能使用動態規劃去解決。 這兩個概念的區別在於: * 分治與動態規劃。 * 分治和DP都是分解為子問題求解,但是分治分解的問題是不重疊的,但是動態規劃解決的問題具有重疊子問題。而且分治解決的問題不一定是最優化問題,而動態規劃解決的問題一定是最優化問題 * 貪心與動態規劃 * 貪心和動態規劃都必須要有最優子結構。二者的計算方式的區別在於,貪心使用的計算方式類似於自頂向下,但是並不是等待子問題求解完畢後再選擇一個,而是通過一種策略直接選擇一個子問題去求解,沒被選擇的問題就直接拋棄了。所以不一定能夠取得最優解。而動態規劃總是會考慮所有的子問題,而且選擇結果更優的子問題,對於暫時沒有被繼承的子問題,由於重疊子問題的存在,後期還可能再會選擇它們,因此還有機會成為全域性最優的一部分,不需要