【資料結構與演算法】 DP 動態規劃 介紹
最近在看演算法導論。
DP全稱是dynamic programming,這裡programming不是程式設計,是一個表格儲存之前的結果。
DP 是一種程式設計思想,主要用於解決最優解型別的問題。
其思路是為了求解當前的問題的最優解,使用子問題的最優解,然後綜合處理,最終得到原問題的最優解。
但是也不是說任何最優解問題都可以DP,使用dp的問題一般滿足下面的兩個特徵:
(1)最優子結構,就是指問題可以通過子問題最優解得到;體現為找出所有的子問題最優解,然後取其中的最優;
(2)重疊子問題,就是子問題是會重複的。而不是一直產生新的子問題(比如分治型別的問題)。
一般而言,滿足上述兩個條件的最優解問題都可以會使用DP來解決。
DP在演算法上的形式是什麼?
有兩種,一種是自頂向下,就是直接從原問題入手,不斷利用子問題來求解,這種寫法是一個遞迴地形式,但是需要加入備忘錄,就是說利用一個數組存已經算出的子問題的結果,下次遇到直接返回。這個思路叫做memoization,備忘錄。是一種空間換時間的做法,因為某些子問題會被呼叫到很多次,如果使用memo,那麼時間上會很高效。比如求斐波那契數列,幾乎每一個求解都會用到f(2)這樣的子問題,如果事先存好,那麼時間複雜度會下降很多。還有一點,memo不是為dp而生的,它也是一種思想或者技巧,在遞迴或者dfs中可以使用,如果要求時間複雜度可以考慮使用memo。
第二種是自底向上,這種不需要遞迴,就是不斷地計算出小問題的解,然後後面的問題就可以利用小問題的解得到。
下面是演算法導論中的一個簡單的例子,給出一個長度為n的鋼管,然後給出切割為不同長度以後的價格,問如何切割獲利最大。
/** * @author miracle *切割鋼條問題: *長度:1 2 3 4 5 6 7 8 9 10 *價格:1 5 8 9 10 17 17 20 24 30 *問長度為n的鋼條的最多賣多少錢 */ public class Solution { int[] prices = {0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30}; int[] dp = new int[prices.length]; public int solve(int[] prices, int n){ if(n == 0) return 0; int max = Integer.MIN_VALUE; for(int i = 1; i <= n; i++){ max = Math.max(max, prices[i] + solve(prices, n - i)); } return max; } public int solveWithMemoUpToBottom(int[] prices, int n){ if(n == 0 || dp[n] > 0) return dp[n]; int max = Integer.MIN_VALUE; for(int i = 1; i <= n; i++){ max = Math.max(max, prices[i] + solve(prices, n - i)); } dp[n] = max; return max; } public int solveBottomToUp(int[] prices, int n){ int[] dp = new int[prices.length]; for(int i = 1; i <= n; i++){ int max = Integer.MIN_VALUE; for(int j = 1; j <= i; j++){ max = Math.max(max, prices[j] + prices[i - j]); } dp[i] = max; } return dp[n]; } public static void main(String args[]){ Solution s = new Solution(); // System.out.println(s.solve(s.prices, 1)); // System.out.println(s.solve(s.prices, 2)); // System.out.println(s.solve(s.prices, 3)); // System.out.println(s.solve(s.prices, 4)); // System.out.println(s.solve(s.prices, 5)); System.out.println(s.solveBottomToUp(s.prices, 1)); System.out.println(s.solveBottomToUp(s.prices, 2)); System.out.println(s.solveBottomToUp(s.prices, 3)); System.out.println(s.solveBottomToUp(s.prices, 4)); System.out.println(s.solveBottomToUp(s.prices, 5)); } }
分別給出了不帶memo,帶memo的以及自底向上3中演算法。
就實際情況來看,一般還是使用非遞迴的bottom to up型別。但是memo在遞迴中的使用也是一個小的技巧。
最後說下遞迴,dp,分治的區別。
遞迴只是一種程式設計的思想,只要自己呼叫自己,就算是遞迴。
分治,有三步,先分,再各自處理,最後整合。這裡也涉及了子問題,這裡的子問題是不重疊的,每一個只被處理一次,因此不需要memo。
dp,可以使用遞迴,而且dp的子問題是重複的。
dp說白了是子問題或者遞迴+memo,他其實是一種brute force,只不過記錄了全部的結果,這就是為什麼dp適用於解決最優解問題的原因(開頭提到),其實它不一定非得解決最優解,只是它的思想使得它非常適合解決最優解問題。