Dynamic programming動態規劃算法

1.什麼是動態規劃算法?

動態規劃算法是指將複雜問題拆分爲簡單問題,並存儲簡單問題的結果,避免重複計算。

也就是說動態規劃算法需要滿足以下3個特點:

  • 1.可以將問題分解爲相似的子問題,並且子問題有重複
  • 2.每一個子問題只解決一次
  • 3.存儲子問題解決後的結果

 

2.什麼樣的問題可以用動態規劃解決?

可以用動態規劃解決的問題主要有如下3個特徵:

  • 1. 重複的子問題
  • 2. 最優子結構
  • 3. 無後效性

重複子問題是指:原問題分解得到的子問題中有完全相同的子問題

最優子結構就是指:原問題的最優解可以通過子問題的最優解的解決而解決

無後效性是指:原問題的解可以通過子問題的解獲得,而不用管子問題的解是如何獲得的 或者說 如果給定某一階段的狀態,則                          在這一階段以後過程的發展不受這階段以前各段狀態的影響。

有點抽象,下面以例子進行說明

2.1以斐波那契數列(Fibonacci)爲例子

從n=0開始的Fibonacci數列:

0 , 1, 1, 2, 3, 5, 8, 13 ,21 ...

下面我們要計算Fibonacci的fib(n)如何計算呢?

首先我們看要計算fib(n)能否拆分爲子問題,在這裏很明顯

fib(n) = fib(n-1) + fib(n-2)

那麼我們要求fib(5),如何求解呢?

我們通過遞歸來求解:

int fib(int n) 
{ 
    if ( n <= 1 ) 
    {
        return n; 
    }
    return fib(n-1) + fib(n-2); 
}
                         fib(5)
                     /             \
               fib(4)                fib(3)
             /      \                /     \
         fib(3)      fib(2)         fib(2)    fib(1)
        /     \        /    \       /    \
  fib(2)   fib(1)  fib(1) fib(0) fib(1) fib(0)
  /    \
fib(1) fib(0)

很明顯,在求解過程中,我們需要求解fib(3)兩次,fib(2)三次。。。。

求解了相同的子問題,每次求解子問題都需要耗費時間,那爲什麼不將子問題的結果存儲起來,再用到相同子問題的時候直接取出來結果就可以,而不用再次計算,從而節省了時間(用空間來換取時間);而這正是動態規劃的意義所在,降低程序時間複雜度,提高程序運行效率

在斐波那契數列求解fib(5)只需求解fib(4)和fib(3),最優子結構這一特徵並不明顯,但我們不用管fib(4)和fib(3)是怎麼求解出來的,直接拿過來用就行了,滿足無後效性。

所以,可以用動態規劃方法來求解。

動態規劃中有兩種不同的方法來存儲子問題的結果:

  • 1.自上而下

針對問題的自上而下存儲子問題結果的程序類似於遞歸版本,只有一個小修改,它在計算解決方案之前會查找存儲表。 我們初始化一個查找數組,所有初始值都爲INT_MAX。 每當我們需要子問題的解決方案時,我們首先查看查找表。 如果預先計算的值在那裏,那麼我們返回該值,否則,我們計算該值並將結果放在查找表中,以便以後可以重用它。

vector<int> look_up(n+1,INT_MAX);//查找表一個包含n+1個元素,因爲有fib(0),全部初始化爲INT_MAX,不爲INT_MAX說明該子問題已經被解決過


int fib(int n) 
{ 
	if (look_up[n] == INT_MAX)//fib(n)還未被求解過 
	{ 
		if (n <= 1) 
			lookup[n] = n; 
		else
			lookup[n] = fib(n - 1) + fib(n - 2); //求解出來後保存到look_up
    } 

    return look_up[n]; //否則,fib(n)已經求解過,再次用的時直接取出返回即可
} 

 

  •     2.  自下而上

自底部向上,一般以迭代的方式進行,給定問題以自下而上的方式構建表,並返回表中的最後一個值。 例如,對於相同的斐波納契數,我們首先計算fib(0)然後計算fib(1)然後計算fib(2)然後計算fib(3),依此類推。 通過建立fib(0)和fib(1)而建立出fib(2)

//迭代的方式自下而上
int fib(int n) 
{ 
    vector<int> look_up(n+1,INT_MAX);
    int i; 
    look_up[0] = 0; 
    look_up[1] = 1; 
    for (i = 2; i <= n; i++)
    { 
	    look_up[i] = look_up[i-1] + look_up[i-2]; 
    }
    return look_up[n]; 
} 

進一步優化:

通過上述程序,我們發現我們求解的目的是fib(n),但是我們保留了大量的中間結果,這是沒必要的,我們只需要保留要求解fib(i)所需要的前兩個狀態fib(i-1)與fib(i-2)

所以,我們可以用兩個變量來代表fib(i)之前的兩個狀態,減少所用的存儲空間

//迭代的方式自下而上,不保留中間結果進行優化
int fib(int n) 
{ 
    
    int i; 
    pre1 = 0; 
    pre2 = 1; 
    res = 0;//fib(n)最後返回結果
    for (i = 1; i <= n; i++)
    { 
	    res = pre1 + pre2; 
        pre1 = pre2;
        pre2 = res;
    }
    return res; 
} 

 

大多數動態規劃問題的解題思路可以按以下步驟進行:

  • 找到遞歸關係(如何找其實就是如何拆分問題,下面會討論)
  • 寫出遞歸程序

然後選擇一種方法優化,熟練之後可以省略寫遞歸程序這一步

  • 遞歸程序+存儲子問題結果   自上而下
  • 迭代程序+存儲子問題結果   自下而上
  • 迭代程序  +  N個變量           自下而上

2.2以最短路徑爲例

我們以最短路徑爲例主要說明最優子結構特性

最優子結構特性是指:原問題的最優解可以通過子問題的最優解推出來

以下圖有向圖爲例,求解節點q到節點t的最短路徑,很明顯是 q -> r -> t或者 q -> s ->t

 

若節點r在最短路徑中,那麼q -> t的最短路徑 就等於 q->r 和 r->t 的最短路徑,拆分爲了兩個子問題的最優解,求出子問題的最優解,那麼原問題的最優解就可以推出來

即最短路徑問題滿足最優子結構特性

但是並不是所有的問題均滿足最優子結構特性,比如最長路徑問題,還是以上圖爲例,

我們要求q -> t的最長路徑,是q -> r -> t或者 q -> s ->t, 

若已知r在最長路徑中,那麼原問題是不能拆分爲 q->r 和 r->t的最長路徑的組合的,

因爲q->r的最長路徑是q->s->t->r,  而r->t的最長路徑是r->q->s->t,兩者結合並不能推出原問題的最優解

所以不符合最優子結構問題

3.怎麼解決動態規劃問題?

解決動態規劃問題的關鍵在於如何拆分問題,這是解決動態規劃問題的關鍵點

解決動態規劃問題時可以按以下思路進行嘗試:

  • 將當前要解決的問題 i 視爲當前狀態,我們的目標是求出 f(i)  ;比如我們求斐波那契數列的第n個數,n就是當前的狀態,f(n)是我們問題的目標
  • 找目標f(i)與那些狀態有關,假設與狀態x有關,找出f(x)與f(i)之間的狀態轉移關係;  比如斐波那契數列目標f(n)與n-1狀態和n-2狀態有關,找出f(n)與f(n-1)與f(n-2)之間的狀態轉移關係, f(n) = f(n-1) + f(n-2)
  • 有了狀態轉移關係就可以很容易運用
  • 動態規劃方法來解決問題

以例子來進行實戰解決動態規劃問題:

  • 上樓梯問題

問題描述:你正在爬樓梯。 你需要n步才能達到頂峯,每次你可以爬1或2步, 您可以通過多少不同的方式登頂?

設當期狀態爲 i,i 表示i步才能到達的地方,我們由 f(i) 種方式到達 i 處,下面我們來看看 f(i) 與哪些狀態有關,由於每次只能爬一步或者兩步,只要到達了 i-1 處或者 i-2  處,我們就可以輕鬆到達 i 處,所以 f(i) 的狀態與 f(i-1) 和 f(i-2)有關,所以狀態轉移關係爲:

                                   f(i) = f(i-1) + f(i-2)

發現沒有,這其實就是上面提到的 斐波那契數列,所以程序不再贅述

  • 搶劫問題

問題描述:

假設你是一個專業的強盜,計劃在街上搶劫房屋。 每個房子都有一定數量的錢存在,阻止你搶劫他們的唯一限制是鄰近的房子連接了安全系統,如果兩個相鄰的房子在同一個晚上被打破,它將自動聯繫警察。給出一個數組nums包括這條街道上房子包含的金額數,確定你今晚可以搶劫的最高金額

比如:[1,2,3,1]  則在不觸發安全系統的前提下你搶的金額最大爲 1 + 3 = 4

現在 假設你已經來到了第 i 間房子處,設你 搶到第 i 間房子處時,總最大金額爲 f(i)

爲了不觸發安全系統,且爲了達到最大淨額,f(i) 的狀態與 f(i-1) 和 f(i - 2)有關,假如你已經搶了第 i-1 間房子,那麼你肯定不能搶第 i 間房, f(i) = f(i-1);但若你搶的是第 i-2 間房,那麼你可以搶第 i 間房,總金額 f(i) = f(i-2) + nums[i] 

nums[i] 表示從第 i 間房搶的金額

所以狀態轉移關係應該爲:

                                         f(i) = max( f(i-1), f(i-2) + nums[i])

自上而下的遞歸程序爲:

class Solution {
public:
    int rob(vector<int>& nums) 
    {
        vector<int> memo(nums.size(),INT_MAX);
        return rob(nums,nums.size()-1,memo);    
    }
private:
    int rob(vector<int>& nums,int n,vector<int>& memo)
    {   
        
        if(n < 0)
        {
            return 0;
        }//遞歸結束條件
        
        if(memo[n] != INT_MAX)
        {
            return memo[n];
        }
            
        int result =  max(rob(nums,n-1,memo),rob(nums,n-2,memo)+nums[n]);
        memo[n] = result;
        return result;
    }
    
  • 必談的最長上升子序列(LIS)問題

給定長度爲n的序列a,從a中抽取出一個子序列,這個子序列需要單調遞增。問最長的上升子序列(LIS)的長度。  

e.g. 1,7,2,8,3,4   中的最長上升子序列爲 1 2 3 4 ,所以長度爲4

現在我們開始拆分問題:假設我們當前狀態爲 i,表示序列a的下標,則我們一定可以找到一個以a[i]爲結尾的上升子序列,(即在這個上升序列中必須以a[i]爲結尾,無論它是否是最長的)

此時LIS爲f(i);下面找f(i)與哪些狀態有關,

很明顯,f(i)與a[i]之前的序列的最長上升序列有關;設之前的最大上升序列爲f(p),p爲a的下標,p可以是i之前的任意下標,而且f(p)是以a[p]爲結尾的最長子序列

那麼如果a[p] < a[i],那麼序列最長上升序列+1

所以寫成狀態轉移方程:

                                     f(i) =  max(f(p))  + 1;       0=< p < i

所以,程序如下:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        if(nums.size() == 0)
        {
            return 0;
        }
        vector<int> look_up(nums.size()+1,1);
        for(int i = 0;i < nums.size();i++)//表示求以a[i]結尾的序列的最長子序列長度
        {
            for(int p = 0;p < i;p++)//將a[i]依次拼接在a[p]後面,判斷最長子序列長度是否變化
            {
                if(nums[p] < nums[i])
                {
                    look_up[i] = max(look_up[i],look_up[p]+1);//選出拼接在a[p]後面的最大長度值
                }   
            }
        }
        int ans = 1;
        //look_up[i]中求的都是以a[i]爲結尾的最長上升子序列長度,所以需要遍歷找出最長的上升子序列長度
        for(int j = 0;j < nums.size();j++)
        {
            ans = max(ans,look_up[j]);
        }
        
        
        return ans;
    }
    
};

 

 

 

 

參考: https://www.geeksforgeeks.org/overlapping-subproblems-property-in-dynamic-programming-dp-1/

           https://www.zhihu.com/question/23995189/answer/35429905

           https://www.zhihu.com/question/23995189

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章