大家好,經過前兩篇的分析,相信大家對動態規劃都有了一定的認識,也能感受到動態規劃強大的演算法思想。今天我們就來總結一下動態規劃能解決哪些問題,以及解決動態規劃問題的思考過程是怎麼樣的?我們一起出發吧。
一、問題模型
動態規劃一般是用來解決最優解。而在解決的過程中是需要經歷多個階段的決策。每個階段都會對應一組狀態。我們需要找到一組決策,經過這些決策後,能求出問題的最優解。我們這類問題抽象成“多階段決策最優模型”。
動態規劃還有三大特徵,分別是最優子結構、無後效性和重複子問題,只有當原問題滿足這三大特徵時,我們才能使用動態規劃的演算法思想來解決。下面我們來分別看一下這三個特徵。
1.最優子結構
最優子結構規定了原問題和子問題的關係。原問題的最優解包含子問題的最優解。反過來說,我們可以通過子問題的最優解來求出原問題的最優解。
2.無後效性
(1)無後效性是指在推導後面階段狀態的時候,只依賴於前面階段的的狀態,而不會去關心這個狀態是怎麼一步步得來的。比如斐波那契數列F(5)=F(4)+F(3),則可以看出F(5)只依賴於F(4)和F(3)這兩個狀態的值,而不用管他們是如何得來的。
(2)一旦某個狀態確定了,就不受之後階段的決策影響。
3.重複子問題
就是原問題經過拆分成多個子問題時,子問題和子問題之間存在重複計算的情況。
下面我們來結合例項,來比較透徹的瞭解一下上面的理論部分。
問題描述:我們假設有一個n*n的矩陣w[n][n](矩陣中儲存正整數)。棋子從矩陣的左上角開始移動到矩陣的右下角。棋子每次只能向下或者向右移動一步。棋子可以有很多不同的路徑走完這個矩 陣。我們把每條路徑經過的數字相加作為路徑長度,那最短的路徑長度是多少呢?
我們從start開始走,一直走到end位置,一共需要走2*(n-1)步,也就對應這2*(n-1)個階段,每個階段都有向下走還是向右走兩種決策,不同的決策對應著不同的狀態。所以這符合多階段決策,而最後求解的是最短路徑長度,所以符合動態規劃的問題模型。
接下來我們看它是否符合動態規劃的三個特徵呢?如下所示,從(0,0)位置走到(1,1)位置有兩種走法,所以符合重複子問題。
下面我們再了看一下無後效性這個特徵。我們要想走到(i,j)這個位置,我們只能通過(i-1,j)和(i,j-1)這兩個位置移動過來,也就是說,我們只需要關心(i-1,j)和(i,j-1)這兩個位置的狀態,而不必關係是如何從(0,0)位置走到這個位置的。而且,我們這裡只允許向下或者向右走,不允許後退,所以前面階段的狀態確定之後,就不會被後面階段的決策所改變。所以這個問題是符合“無後效性”這個特徵的。
我們把從(0,0)位置走到(i,j)位置的最短路徑記為為min(i,j),因為我們只能向右或者向後移動,所以我們只能從(i-1,j)和(i,j-1)兩個位置到達(i,j)。換句話說,就是min(i,j)只能通過min(i-1,j)和min(i,j-1)兩個狀態推匯出來。所以這個問題就符合“最優子結構”。
min(i,j)=w[i][j]+min(min(i-1,j),min(i,j-1))
所以這個問題是符合動態規劃問題模型的,所以我們可以採用動態規劃思想來解決這個問題。
二、解決思路
解決動態規劃問題,一般有狀態轉移表法和狀態轉移方程法這兩種方法。
1.狀態轉移表法:
我們先定義一個狀態表,一般狀態表都是二維的。我們根據決策的先後順序,從前往後,根據遞迴關係,分階段的填充狀態表中的每個狀態。最後,我們把遞迴填表的過程翻譯成程式碼就完成了。我們接下來看如何用狀態轉移表法來求最短路徑這個問題的。我們畫出一個二維陣列,表中的行、列表示棋子所在的位置,表中的數值表示從起點到這個位置的最短路徑。然後,我們按照決策過程依次去填表,前兩步的走法如下圖所示:
我們來看一下程式碼如何實現。
def minDis(data,n): status=[[0 for _ in range(n)] for _ in range(n)] sum=0 #第一行賦值 for i in range(n): sum=sum+data[0][i] status[0][i]=sum #第一列賦值 sum=0 for i in range(n): sum=sum+data[i][0] status[i][0]=sum for i in range(1,n): for j in range(1,n): status[i][j]=data[i][j]+min(status[i-1][j],status[i][j-1]) return status[n-1][n-1] data=[[1,3,5,7,2], [3,6,5,2,1], [7,4,1,6,5], [1,3,8,2,3], [4,3,1,6,4]] n=5 print(minDis(data,n))
2.狀態轉移方程法
狀態轉移方程法和遞迴的解題思路類似。我們根據最優子結構,寫出遞推公式,也就是所謂的狀態轉移方程。然後根據狀態轉移方程,實現程式碼就好了。這裡一般有兩種實現方法,一種是遞迴加備忘錄方法,另一種是迭代遞推。
我們看這個最短路徑的例子,它的狀態轉移方程如下所示:
min(i,j)=w[i][j]+min(min(i-1,j),min(i,j-1))
我們這裡採用遞迴加備忘錄方法來實現,另一種迭代遞推的實現和狀態轉移表法的程式碼實現是一致的,只是思路不同而已。
import sys data=[[1,3,5,7,2], [3,6,5,2,1], [7,4,1,6,5], [1,3,8,2,3], [4,3,1,6,4]] n=5 mem=[[0 for _ in range(5)] for _ in range(5)] def minDis(i,j): if i==0 and j==0: return data[0][0] if mem[i][j]>0: return mem[i][j] minLeft = sys.maxsize if j-1>=0: minLeft=minDis(i,j-1) minUp = sys.maxsize if i-1>=0: minUp=minDis(i-1,j) current=data[i][j]+min(minLeft,minUp) mem[i][j]=current return current print(minDis(n-1,n-1))
到這裡為止,我們的動態規劃就聊完了。希望你已經對動態規劃演算法思想有所掌握。如果沒有明白也沒關係,多做幾道題,然後回過頭來再看,一定會有收穫的。
更多硬核知識,請關注公眾號。