動態規劃的詳細分析

動態規劃相信你們都知道,動態規劃算法也是新手在剛接觸算法設計時很苦惱的問題,有時候以爲難以理解,可是真正理解以後,就會以爲動態規劃其實並無想象中那麼難。網上也有不少關於講解動態規劃的文章,大多都是敘述概念,講解原理,讓人以爲晦澀難懂,即便一時間看懂了,發現當本身作題的時候又會以爲無所適從。我以爲,理解算法最重要的仍是在於練習,只有經過本身練習,才能夠更快地提高。話很少說,接下來,下面我就經過一個例子來一步一步講解動態規劃是怎樣使用的,只有知道怎樣使用,才能更好地理解,而不是一味地對概念和原理進行反覆琢磨。ios

    首先,咱們看一下這道題(此題目來源於北大POJ):算法

    數字三角形(POJ1163)數組

   

    在上面的數字三角形中尋找一條從頂部到底邊的路徑,使得路徑上所通過的數字之和最大。路徑上的每一步都只能往左下或 右下走。只須要求出這個最大和便可,沒必要給出具體路徑。 三角形的行數大於1小於等於100,數字爲 0 - 99函數

    輸入格式:優化

    5      //表示三角形的行數    接下來輸入三角形spa

    7.net

    3   8設計

    8   1   0blog

    2   7   4   4遞歸

    4   5   2   6   5

    要求輸出最大和

    接下來,咱們來分析一下解題思路:

    首先,確定得用二維數組來存放數字三角形

    而後咱們用D( r, j) 來表示第r行第 j 個數字(r,j從1開始算)

    咱們用MaxSum(r, j)表示從D(r,j)到底邊的各條路徑中,最佳路徑的數字之和。

    所以,此題的最終問題就變成了求 MaxSum(1,1)

    當咱們看到這個題目的時候,首先想到的就是能夠用簡單的遞歸來解題:

    D(r, j)出發,下一步只能走D(r+1,j)或者D(r+1, j+1)。故對於N行的三角形,咱們能夠寫出以下的遞歸式:   

if ( r == N)
MaxSum(r,j) = D(r,j)
else
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)
    根據上面這個簡單的遞歸式,咱們就能夠很輕鬆地寫出完整的遞歸代碼: 

#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int n;
int MaxSum(int i, int j){
if(i==n)
return D[i][j];
int x = MaxSum(i+1,j);
int y = MaxSum(i+1,j+1);
return max(x,y)+D[i][j];
}
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
cout << MaxSum(1,1) << endl;
}
    對於如上這段遞歸的代碼,當我提交到POJ時,會顯示以下結果:

   

    對的,代碼運行超時了,爲何會超時呢?

    答案很簡單,由於咱們重複計算了,當咱們在進行遞歸時,計算機幫咱們計算的過程以下圖:

    

    就拿第三行數字1來講,當咱們計算從第2行的數字3開始的MaxSum時會計算出從1開始的MaxSum,當咱們計算從第二行的數字8開始的MaxSum的時候又會計算一次從1開始的MaxSum,也就是說有重複計算。這樣就浪費了大量的時間。也就是說若是採用遞規的方法,深度遍歷每條路徑,存在大量重複計算。則時間複雜度爲 2的n次方,對於 n = 100 行,確定超時。 

    接下來,咱們就要考慮如何進行改進,咱們天然而然就能夠想到若是每算出一個MaxSum(r,j)就保存起來,下次用到其值的時候直接取用,則可免去重複計算。那麼能夠用n方的時間複雜度完成計算。由於三角形的數字總數是 n(n+1)/2

    根據這個思路,咱們就能夠將上面的代碼進行改進,使之成爲記憶遞歸型的動態規劃程序: 

#include <iostream>
#include <algorithm>
using namespace std;

#define MAX 101

int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];

int MaxSum(int i, int j){
if( maxSum[i][j] != -1 )
return maxSum[i][j];
if(i==n)
maxSum[i][j] = D[i][j];
else{
int x = MaxSum(i+1,j);
int y = MaxSum(i+1,j+1);
maxSum[i][j] = max(x,y)+ D[i][j];
}
return maxSum[i][j];
}
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++) {
cin >> D[i][j];
maxSum[i][j] = -1;
}
cout << MaxSum(1,1) << endl;
}
    當咱們提交如上代碼時,結果就是一次AC

   

    雖然在短期內就AC了。可是,咱們並不能知足於這樣的代碼,由於遞歸老是須要使用大量堆棧上的空間,很容易形成棧溢出,咱們如今就要考慮如何把遞歸轉換爲遞推,讓咱們一步一步來完成這個過程。

    咱們首先須要計算的是最後一行,所以能夠把最後一行直接寫出,以下圖:

   

    如今開始分析倒數第二行的每個數,現分析數字2,2能夠和最後一行4相加,也能夠和最後一行的5相加,可是很顯然和5相加要更大一點,結果爲7,咱們此時就能夠將7保存起來,而後分析數字7,7能夠和最後一行的5相加,也能夠和最後一行的2相加,很顯然和5相加更大,結果爲12,所以咱們將12保存起來。以此類推。。咱們能夠獲得下面這張圖:

   

    而後按一樣的道理分析倒數第三行和倒數第四行,最後分析第一行,咱們能夠依次獲得以下結果:

   

   

    上面的推導過程相信你們不難理解,理解以後咱們就能夠寫出以下的遞推型動態規劃程序: 

#include <iostream>
#include <algorithm>
using namespace std;

#define MAX 101

int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
for( int i = 1;i <= n; ++ i )
maxSum[n][i] = D[n][i];
for( int i = n-1; i>= 1; --i )
for( int j = 1; j <= i; ++j )
maxSum[i][j] = max(maxSum[i+1][j],maxSum[i+1][j+1]) + D[i][j];
cout << maxSum[1][1] << endl;
}
     咱們的代碼僅僅是這樣就夠了嗎?固然不是,咱們仍然能夠繼續優化,而這個優化固然是對於空間進行優化,其實徹底不必用二維maxSum數組存儲每個MaxSum(r,j),只要從底層一行行向上遞推,那麼只要一維數組maxSum[100]便可,即只要存儲一行的MaxSum值就能夠。

     對於空間優化後的具體遞推過程以下:

   

   

   

   

   

   

    接下里的步驟就按上圖的過程一步一步推導就能夠了。進一步考慮,咱們甚至能夠連maxSum數組均可以不要,直接用D的第n行直接替代maxSum便可。可是這裏須要強調的是:雖然節省空間,可是時間複雜度仍是不變的。

    依照上面的方式,咱們能夠寫出以下代碼:    

 

#include <iostream>
#include <algorithm>
using namespace std;

#define MAX 101

int D[MAX][MAX];
int n;
int * maxSum;

int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
maxSum = D[n]; //maxSum指向第n行
for( int i = n-1; i>= 1; --i )
for( int j = 1; j <= i; ++j )
maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j];
cout << maxSum[1] << endl;
}
 

 

 

 

 

    接下來,咱們就進行一下總結:

    遞歸到動規的通常轉化方法

    遞歸函數有n個參數,就定義一個n維的數組,數組的下標是遞歸函數參數的取值範圍,數組元素的值是遞歸函數的返回值,這樣就能夠從邊界值開始, 逐步填充數組,至關於計算遞歸函數值的逆過程。

    動規解題的通常思路

    1. 將原問題分解爲子問題

    把原問題分解爲若干個子問題,子問題和原問題形式相同或相似,只不過規模變小了。子問題都解決,原問題即解決(數字三角形例)。
    子問題的解一旦求出就會被保存,因此每一個子問題只需求 解一次。
    2.肯定狀態

    在用動態規劃解題時,咱們每每將和子問題相關的各個變量的一組取值,稱之爲一個「狀 態」。一個「狀態」對應於一個或多個子問題, 所謂某個「狀態」下的「值」,就是這個「狀 態」所對應的子問題的解。
    全部「狀態」的集合,構成問題的「狀態空間」。「狀態空間」的大小,與用動態規劃解決問題的時間複雜度直接相關。 在數字三角形的例子裏,一共有N×(N+1)/2個數字,因此這個問題的狀態空間裏一共就有N×(N+1)/2個狀態。
    整個問題的時間複雜度是狀態數目乘以計算每一個狀態所需時間。在數字三角形裏每一個「狀態」只須要通過一次,且在每一個狀態上做計算所花的時間都是和N無關的常數。

    3.肯定一些初始狀態(邊界狀態)的值

    以「數字三角形」爲例,初始狀態就是底邊數字,值就是底邊數字值。

    4. 肯定狀態轉移方程

     定義出什麼是「狀態」,以及在該「狀態」下的「值」後,就要找出不一樣的狀態之間如何遷移――即如何從一個或多個「值」已知的 「狀態」,求出另外一個「狀態」的「值」(遞推型)。狀態的遷移能夠用遞推公式表示,此遞推公式也可被稱做「狀態轉移方程」。

    數字三角形的狀態轉移方程:

   
 

    能用動規解決的問題的特色

    1) 問題具備最優子結構性質。若是問題的最優解所包含的 子問題的解也是最優的,咱們就稱該問題具備最優子結 構性質。

    2) 無後效性。當前的若干個狀態值一旦肯定,則此後過程的演變就只和這若干個狀態的值有關,和以前是採起哪一種手段或通過哪條路徑演變到當前的這若干個狀態,沒有關係。————————————————版權聲明:本文爲CSDN博主「ChrisYoung1314」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。原文連接:https://blog.csdn.net/baidu_28312631/article/details/47418773

相關文章
相關標籤/搜索