動態規劃你學會了嗎?

公眾號程式設計師學長發表於2021-08-13

動態規劃一直被認為是最難理解的一種演算法思想,什麼重疊子問題、動態轉移方程、最優子結構等等,一聽就高深莫測,沒有往下學習下去的動力。接下了我會更新一系列的文章來把動態規劃這個演算法思想盡量去講明白,希望對你在以後的學習生活中提供一些幫助。沒有關注的同學先點個關注吧。

一、初識動態規劃

  廢話不多說,我們直接先上一個經典的例子。那就是耳熟能詳的斐波那契數列問題。我們先來看一下問題的定義。

斐波那契數列的定義如下:   
斐波那契數列指的是這樣一個數列 0,1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,.....  
它以遞迴的方法來定義:  
F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
複製程式碼
  1. 遞迴解決:

     這個例子最直觀的方法就是用遞迴的方式來實現,畢竟斐波那契數列是用遞迴來定義的。我們來看一下程式碼實現。

def fibs(n):
     if n<2:
          return n
     return fibs(n-1)+fibs(n-2)
     
複製程式碼

  這樣是不是很簡單。我們接下來看一下呼叫的遞迴樹。我們以fibs(6)為例。

圖片

  其中每個結點表示要計算的斐波那契數列的第幾項,我們可以從上圖發現,會出現許多重複計算的問題,比如fib(4)就計算了兩次。這樣就會帶來時間和空間上的消耗,那我們有什麼方式可以避免重複計算的問題。我們可以使用遞迴中的“備忘錄”功能來解決。我們來看一下程式碼如何實現。\

class Fib:
     def flibs(self,n):
         #備忘錄資料,用來存放已經計算過的項
         self.catch=[-1 for i in range(n+1)]
         print(self.catch)
         return self._fibs(n)
     def _fibs(self,n):
          if n<2:
               if self.catch[n]==-1:
                    self.catch[n]=n
               return self.catch[n]
          if self.catch[n-1]==-1:
               self.catch[n-1]=self._fibs(n-1)
          if self.catch[n-2]==-1:
               self.catch[n-2]=self._fibs(n-2)
          self.catch[n]=self.catch[n-1]+self.catch[n-2]
          return self.catch[n]

f=Fib()

print(f.flibs(6))
print(f.catch)
複製程式碼

這種方法可以減低重複計算的問題,這已經和動態規劃的效率差不多了,但是這程式碼看起來非常冗餘,也不是所有問題都能用這種方法來解決的。下面我們就來看一下動態規劃。\

二、用動態規劃解決

   我們把整個求解過程分為n個階段,每個階段去求解數列對應項的值。我們在解決當前問題時,也就是求解該對應項的值的時候,會依賴過去的狀態,也就是前面幾項的值來計算。比如我們在求解fibs(6)的時候,我們需要用到fibs(5)和fibs(4)這兩項。 我們來定義一個陣列,來記錄每項的狀態。我們也叫做狀態轉移矩陣。 按照斐波那契數列的定義:

F(0)=0,F(1)=1
F(n)=F(n-1)+F(n-2) (n>=2)
複製程式碼

我們可以看到F(n)的值只與他的前兩個狀態有關。所以我們只要知道他的前兩個狀態,就可以求出F(n)。

  1. 初始化值F(0)=0,F(1)=1,我們直接放入陣列中。
  2. 要想計算F(2),我們需要知道F(0)和F(1),因為上一步已經放入陣列中,我們直接拿來用就好了,然後把F(2)的結果放入陣列中。
  3. 要想計算F(3),我們需要知道F(2)和F(1),因為F(2)和F(1)已經存在陣列裡了,我們直接拿來用就好了,然後把F(3)的結果放入陣列中。

    ....

 依此類推,知道計算到n為止。整個狀態轉移矩陣就計算好了。如下圖所示。我們以求解F(5)為例。

圖片

圖片

 下面我們直接看程式碼實現,這樣比較簡單明瞭。

def fibs(n):
     if n<2:
          return n
     dp=[0 for _ in range(n+1)]
     dp[0]=0
     dp[1]=1
     for i in range(2,n+1):
          dp[i]=dp[i-1]+dp[i-2]
     return dp[n]
print(fibs(6))
複製程式碼

  上面的程式碼是不是很簡潔明瞭。這就是一種用動態規劃來解決問題的思路。我們把問題分解為n個階段,一個階段一個階段去求解。然後通過當前狀態,來求出下一個狀態,動態的往前推進,這是不是還挺形象的。今天我們就先分享到這裡,下一篇我們從0、1揹包問題出發,來吃透動態規劃。為了不錯過,記得點個關注呀。

今天我們沒有涉及太多的理論知識,只是從一個簡單的問題入手,體驗了一下動態規劃的強大思想。其實,大部分動態規劃能解決的問題,都可以用回溯演算法來解決,如果不熟悉回溯演算法,可以看從八皇后問題到回溯演算法這篇文章。只不過回溯演算法解決起來效率比較低,時間複雜度較高。動態規劃演算法,在執行效率上會高很多,但是為了儲存每一步的狀態,空間複雜度也相應的提高了。所以,很多時候我們會說,動態規劃是一種空間換時間的演算法思想。

更多硬核知識,請關注公眾號。

 

相關文章