什麼是動態規劃?

最近在嘗試着幫助個人朋友理解動態規劃,我在網上找了很久,相關的資料有不少,可是大多時候直接引用了維基百科對動態規劃的定義,而後直接對着問題擼代碼,我以爲光注重代碼實現,是不能很好地將思想傳授給其餘學習者的。java

爲了讓你們可以更輕鬆地認識動態規劃,同時我也想把我本身學習動態規劃的一些過程跟理解記錄下來,這篇文章是動態規劃系列的第一篇,我將盡力在本文中把動態規劃的本質跟它所解決的問題講清楚。面試

斐波那契數列

能應用動態規劃的問題有不少,但我以爲最經典的,能讓人快速對它有一個朦朧的認識的問題就是求解斐波那契數列。不少人可能對斐波那契數列還不是很瞭解,簡單來講,它就是以0,1開頭的一個數列,以後的每一位都是前兩位之和。舉個例子,0,1,1,2,3,5,8,13,21,...數組

咱們用公式把它列出來:緩存

Fib(n) = Fib(n-1) + Fib(n-2), for n > 1
Fib(0) = 0, Fib(1) = 1
複製代碼

咱們能夠很輕易地把這個公式轉化成代碼:bash

public int fib(int n) {
    if (n < 2)
        return n;
 return fib(n - 1) + fib(n - 2); 
 }
複製代碼

經過遞歸可以獲得咱們想要的答案,爲了計算當前結果咱們會轉而先去計算前兩個位置的結果Fn-1,Fn-2,最後結合起來就能獲得Fn。可是有一個問題,此時的時間複雜度是O(2^n),空間複雜度是O(n),隨着輸入數字的變大,咱們獲得結果要等待的時間會增長,這個時間很大一部分浪費在重複計算上。假設咱們輸入是n=5,咱們看一看下面一張圖: 學習

求解過程分解
咱們能夠看到,光是計算F5,咱們就進行了不少重複計算。那怎麼解決呢?

思路一:

工程上的經驗告訴咱們,當一個先前獲取的結果後面還有可能用到而且每一個相同的輸入返回的結果都相同時,咱們能夠把這個結果緩存起來。這樣,當緩存中存在對於某個輸入的結果時,咱們能夠跳開計算直接從緩存返回那個結果,否則咱們得先計算,而後把結果放到緩存中,以備下次須要的時候使用。代碼以下:優化

public int fib(int n) {
    int dp[] = new int[n + 1];//使用數組緩存結果
 return fibRecursive(dp, n); 
 }

public int fibRecursive(int[] dp, int n) {
    if (n < 2)
        return n;
 if (dp[n] == 0)
        dp[n] = fibRecursive(dp, n - 1) + fibRecursive(dp, n - 2);
 return dp[n]; 
 }
複製代碼

這個時候咱們已經能把代碼優化到時間空間複雜度都是O(n),就結果而言,這對咱們來講是個不小的突破。spa

那時間空間複雜度還能不能更低了?咱們來試試看!3d

思路二:

咱們再回過來仔細觀察一下這個數列:code

斐波那契數列
咱們能夠發現後面的結果只依賴前面的兩個數。也就是說咱們在求解5對應的結果的時候,只要知道3跟4對應的結果就行了。而F0跟F1是已知的,經過它倆能夠計算出F2,而後又能夠計算出F3,以此類推。(注意,這裏跟上面的解決方案聽着類似實則有個重要的區別,上面的方案是在求當前數對應結果的過程當中去算前兩個數的結果,這裏的思路是已經事先算出了前兩個數的結果,能夠直接拿來組合出當前須要的結果)

那咱們實現就很清晰了:

public int fib(int n) {
    int dp[] = new int[n + 1];
  dp[0] = 0;
  dp[1] = 1;   for (int i = 2; i <= n; i++)
        dp[i] = dp[i - 1] + dp[i - 2];   
  return dp[n]; 
}
複製代碼

此時時間空間複雜度還都是O(n),細心的同窗可能已經發現了,對於這個問題,其實咱們須要且只須要前兩個數,更早以前的結果其實用完就沒必要緩存了,能夠丟棄掉,(也就是說,當已經計算出F4的時候,還緩存着F0~F2其實沒有意義,只會浪費空間,咱們不會再用到它們了)那咱們就沒必要再維護一個緩存數組,使用兩個變量來存儲前兩個數就足夠了,這樣就把空間複雜度降低到一個常量級O(1)。

public int fib(int n) {
    if (n < 2)
        return n;
 int n1 = 0, n2 = 1, temp;
 for (int i = 2; i <= n; i++) {
        temp = n1 + n2;
  n1 = n2;
  n2 = temp;
  }
  return n2; 
}
複製代碼

到這裏咱們對斐波那契數列問題的探討就結束了。不少人可能要納悶了,???怎麼徹底沒提到動態規劃呀,哈哈哈,其實咱們上面思考過程就是在用動態規劃解決問題啦,就是這麼簡單,稍稍總結下,所謂動態規劃本質上就是對遞歸進行優化的一種方法,而動態規劃問題有兩個顯著的特徵,

  1. 它有不少重複的子問題(遞歸中遇到重複計算);
  2. 基於子問題的結果能夠得出原問題的結果。

而後根據這兩個特徵咱們也衍生出了兩種優化思路:

  1. 解決當前問題的過程當中解決包含的子問題,並把已解決的問題結果緩存起來。(思路一)
  2. 先解決子問題,而後直接合並涉及到的子問題的結果產生當前問題的結果。(思路二)

好了,相信你們對動態規劃都已經有了個大體的感知了,以前提到動態規劃好多人都很怕啊,以爲面試遇到這種問題就確定涼了。其實沒必要慌張,拿到動態規劃問題後把它分解成更小更簡單的子問題,而後應用咱們上面的兩種思路解決問題便可。我知道一個問題到手最難的實際上是判別它是否是動態規劃問題,或者說有些動態規劃問題的子問題可能不像斐波那契數列這麼好識別,在後續的文章裏,我也會列舉出常見的幾種動態規劃題目類型,幫助你們加強認識。

快來關注我吧
相關文章
相關標籤/搜索