幾個經典的動態規劃算法

一、動態規劃基本思想

    一般來說,只要問題可以劃分成規模更小的子問題,並且原問題的最優解中包含了子問題的最優解,則可以考慮用動態規劃解決。動態規劃的實質是分治思想和解決冗餘,因此,動態規劃是一種將問題實例分解爲更小的、相似的子問題,並存儲子問題的解而避免計算重複的子問題,以解決最優化問題的算法策略。由此可知,動態規劃法與分治法和貪心法類似,它們都是將問題實例歸納爲更小的、相似的子問題,並通過求解子問題產生一個全局最優解。其中貪心法的當前選擇可能要依賴已經作出的所有選擇,但不依賴於有待於做出的選擇和子問題。因此貪心法自頂向下,一步一步地作出貪心選擇;而分治法中的各個子問題是獨立的 (即不包含公共的子子問題),因此一旦遞歸地求出各子問題的解後,便可自下而上地將子問題的解合併成問題的解。但不足的是,如果當前選擇可能要依賴子問題的解時,則難以通過局部的貪心策略達到全局最優解;如果各子問題是不獨立的,則分治法要做許多不必要的工作,重複地解公共的子問題。解決上述問題的辦法是利用動態規劃。該方法主要應用於最優化問題,這類問題會有多種可能的解,每個解都有一個值,而動態規劃找出其中最優(最大或最小)值的解。若存在若干個取最優值的解的話,它只取其中的一個。在求解過程中,該方法也是通過求解局部子問題的解達到全局最優解,但與分治法和貪心法不同的是,動態規劃允許這些子問題不獨立,也允許其通過自身子問題的解作出選擇,該方法對每一個子問題只解一次,並將結果保存起來,避免每次碰到時都要重複計算因此,動態規劃法所針對的問題有一個顯著的特徵,即它所對應的子問題樹中的子問題呈現大量的重複。動態規劃法的關鍵就在於,對於重複出現的子問題,只在第一次遇到時加以求解,並把答案保存起來,讓以後再遇到時直接引用,不必重新求解。

中的子問題呈現大量的重複。

動態規劃法的關鍵就在於,

對於重複出現的子問題,

只在第一次遇到時加以求解,

並把答案保存起來,

讓以後再遇到時直接引用,

必重新求解。

中的子問題呈現大量的重複。

動態規劃法的關鍵就在於,

對於重複出現的子問題,

只在第一次遇到時加以求解,

並把答案保存起來,

讓以後再遇到時直接引用,

必重新求解。

中的子問題呈現大量的重複。

動態規劃法的關鍵就在於,

對於重複出現的子問題,

只在第一次遇到時加以求解,

並把答案保存起來,

讓以後再遇到時直接引用,

必重新求解。


二、動態規劃實例

1、0-1揹包問題

問題描述

    假設我們有n件物品,分別編號爲1,2,…n。其中編號爲i的物品價值Vi,它的重量爲Wi。爲了簡化問題,假設價值和重量都是整數。現在假設我們有一個揹包,它能夠承載的重量是W。現在,我們希望王揹包裏裝這些物品,使得包裏裝的物品價值最大化,那麼我們該如何選擇裝的物品?問題結構如下圖所示:


初步分析

    對於這個問題,一開始確實有點不太好入手。一堆的物品,每一個都有一定的質量和價值,我們能夠裝入的總重量有限制,該怎麼來裝使得價值最大呢?對於這n個物品,每個物品我們可能會選,也可能不選,那麼我們總共就可能有2^n種組合選擇方式。如果我們採用這種辦法來硬算的話,則整體的時間複雜度就達到指數級別的,肯定不可行。
現在我們換一種思路。既然每一種物品都有價格和重量,我們優先挑選那些單位價格最高的是否可行呢?比如在下圖中,我們有3種物品,他們的重量和價格分別是10, 20, 30 kg和60, 100, 120


    那麼按照單位價格來算的話,我們最先應該挑選的是價格爲60的元素,選擇它之後,揹包還剩下50 - 10 = 40kg。再繼續前面的選擇,我們應該挑選價格爲100的元素,這樣揹包裏的總價值爲60 + 100 = 160。所佔用的重量爲30, 剩下20kg。因爲後面需要挑選的物品爲30kg已經超出揹包的容量了。我們按照這種思路能選擇到的最多就是前面兩個物品。如下圖:


    按照我們前面的期望,這樣選擇得到的價值應該是最大的。可是由於有一個揹包重量的限制,這裏只用了30kg,還有剩下20kg浪費了。這會是最優的選擇嗎?我們看看所有的選擇情況:


    很遺憾,在這幾種選擇情況中,我們前面的選擇反而是帶來價值最低的。而選擇重量分別爲20kg和30kg的物品帶來了最大的價值。看來,我們剛纔這種選擇最佳單位價格的方式也行不通。

動態規劃解決問題

  既然前面兩種辦法都不可行,我們再來看看有沒有別的方法。我們再來看這個問題。我們需要選擇n個元素中的若干個來形成最優解,假定爲k個。那麼對於這k個元素a1, a2, …ak來說,它們組成的物品組合必然滿足總重量<=揹包重量限制,而且它們的價值必然是最大的。因爲它們是我們假定的最優選擇嘛,肯定價值應該是最大的。假定ak是我們按照前面順序放入的最後一個物品。它的重量爲wk,它的價值爲vk。既然我們前面選擇的這k個元素構成了最優選擇,如果我們把這個ak物品拿走,對應於k-1個物品來說,它們所涵蓋的重量範圍爲0-(W-wk)。假定W爲揹包允許承重的量。假定最終的價值是V,剩下的物品所構成的價值爲V-vk。這剩下的k-1個元素是不是構成了一個這種W-wk的最優解呢?

    我們可以用反證法來推導。假定拿走ak這個物品後,剩下的這些物品沒有構成W-wk重量範圍的最佳價值選擇。那麼我們肯定有另外k-1個元素,他們在W-wk重量範圍內構成的價值更大。如果這樣的話,我們用這k-1個物品再加上第k個,他們構成的最終W重量範圍內的價值就是最優的。這豈不是和我們前面假設的k個元素構成最佳矛盾了嗎?所以我們可以肯定,在這k個元素裏拿掉最後那個元素,前面剩下的元素依然構成一個最佳解。

    現在我們經過前面的推理已經得到了一個基本的遞推關係,就是一個最優解的子解集也是最優的。可是,我們該怎麼來求得這個最優解呢?我們這樣來看。假定我們定義一個函數c[i, w]表示到第i個元素爲止,在限制總重量爲w的情況下我們所能選擇到的最優解。那麼這個最優解要麼包含有i這個物品,要麼不包含,肯定是這兩種情況中的一種。如果我們選擇了第i個物品,那麼實際上這個最優解是c[i - 1, w-wi] + vi。而如果我們沒有選擇第i個物品,這個最優解是c[i-1, w]。這樣,實際上對於到底要不要取第i個物品,我們只要比較這兩種情況,哪個的結果值更大不就是最優的麼?

    在前面討論的關係裏,還有一個情況我們需要考慮的就是,我們這個最優解是基於選擇物品i時總重量還是在w範圍內的,如果超出了呢?我們肯定不能選擇它,這就和c[i-1, w]一樣。

    另外,對於初始的情況呢?很明顯c[0, w]裏不管w是多少,肯定爲0。因爲它表示我們一個物品都不選擇的情況。c[i, 0]也一樣,當我們總重量限制爲0時,肯定價值爲0。

    這樣,基於我們前面討論的這3個部分,我們可以得到一個如下的遞推公式:


    有了這個關係,我們可以更進一步的來考慮代碼實現了。我們有這麼一個遞歸的關係,其中,後面的函數結果其實是依賴於前面的結果的。我們只要按照前面求出來最基礎的最優條件,然後往後面一步步遞推,就可以找到結果了。

    我們再來考慮一下具體實現的細節。這一組物品分別有價值和重量,我們可以定義兩個數組int[] v, int[] w。v[i]表示第i個物品的價值,w[i]表示第i個物品的重量。爲了表示c[i, w],我們可以使用一個int[i][w]的矩陣。其中i的最大值爲物品的數量,而W表示最大的重量限制。按照前面的遞推關係,c[i][0]和c[0][w]都是0。而我們所要求的最終結果是c[n][w]。所以我們實際中創建的矩陣是(n + 1) x (W + 1)的規格。

具體數據:物品個數n = 5,物品重量w[n] = {0,2,2,6,5,4},物品價值v[n] = {0,6,3,5,4,6},W = 10。

代碼實現

  1. #include <iostream>  
  2. #define  max(a,b)((a)>(b)?a:b)  
  3. using namespace std;  
  4.   
  5. int Weight[] = {0,2,2,6,5,4};  //物品的重量  
  6. int Value[] = {0,6,3,5,4,6};  //物品的價值  
  7. int table[6][11];     //存放表示到第i個元素爲止,在限制總重量爲w的情況下我們所能選擇到的最優解  
  8. bool Frist_Flag = true;  //主要是初始化table[][] = Value[1]  
  9. int main(void)  
  10. {  
  11.     for (int i = 1;i <= 5;i++)  
  12.     {  
  13.         for (int j = 1;j <=10;j++)  
  14.         {  
  15.             if (Frist_Flag == true)  
  16.             {  
  17.                 if (Weight[1] <= j)  
  18.                 {  
  19.                     table[i][j] = Value[1];  
  20.                     Frist_Flag = false;  
  21.                 }  
  22.                       
  23.             }  
  24.             else  
  25.             {  
  26.                 if (Weight[i] > j)  
  27.                 {  
  28.                     table[i][j] = table[i-1][j];  
  29.                 }   
  30.                 else  
  31.                 {  
  32.                     table[i][j] = max(table[i-1][j-Weight[i]]+Value[i],table[i-1][j]);  
  33.                 }  
  34.             }  
  35.         }  
  36.     }  
  37.     cout << ”MaxValue = ”<< table[5][10] << endl;  
  38.     return 0;  
  39.   
  40. }  
#include <iostream>




#define max(a,b)((a)>(b)?a:b) using namespace std; int Weight[] = {0,2,2,6,5,4}; //物品的重量 int Value[] = {0,6,3,5,4,6}; //物品的價值 int table[6][11]; //存放表示到第i個元素爲止,在限制總重量爲w的情況下我們所能選擇到的最優解 bool Frist_Flag = true; //主要是初始化table[][] = Value[1] int main(void) { for (int i = 1;i <= 5;i++) { for (int j = 1;j <=10;j++) { if (Frist_Flag == true) { if (Weight[1] <= j) { table[i][j] = Value[1]; Frist_Flag = false; } } else { if (Weight[i] > j) { table[i][j] = table[i-1][j]; } else { table[i][j] = max(table[i-1][j-Weight[i]]+Value[i],table[i-1][j]); } } } } cout << "MaxValue = "<< table[5][10] << endl; return 0; }

2、最大子數組和問題

問題

一個有N個整數元素的一維數組(A[0],A[1],…A[N-1]),這個數組有很多子數組,求子數組和的最大值?注意:子數組必須是連續的、不需要返回子數組的具體位置、數組中包含:正、負、零整數、子數組不能空。

例如:

int A[5] = {-1,2,3,-4,2};

符合條件的子數組爲2,3,即答案爲5;

窮舉法:

  1. int MaxSubStringSum(int *A,int n)  
  2. {  
  3.     int MaxSum = A[0];  
  4.     int sum = 0;  
  5.     for (int i = 0;i < n;i++)  
  6.     {  
  7.         sum = 0;  
  8.         for (int j = i;j < n;j++)  
  9.         {  
  10.             sum += A[j];  
  11.             MaxSum = max(MaxSum,sum);  
  12.   
  13.         }  
  14.     }  
  15.     return MaxSum;  
  16. }  
int MaxSubStringSum(int *A,int n)
{
    int MaxSum = A[0];
    int sum = 0;
    for (int i = 0;i < n;i++)
    {
        sum = 0;
        for (int j = i;j < n;j++)
        {
            sum += A[j];
            MaxSum = max(MaxSum,sum);

        }
    }
    return MaxSum;
}

窮取法最爲直接,當然耗時也較多,時間複雜度爲O(n^2);

進一步分析

我們利用窮舉法雖然簡單易懂,但是其時間複雜度很大,我們試着優化。現在考慮數組的第一個元素A[0],與和最大的子數組(A[i],……A[j])之間的關係,有以下三種關係:

 1) i = j = 0;A[0]本身構成和最大的子數組

 2) j > i = 0;和最大的子數組以A[0]開頭

 3) i > 0;A[0]與和最大子數組沒有關係

從上面3中情況可以看出,可以將一個大問題(N個元素的數組)轉化爲一個較小的問題(N - 1個元素的數組)。假設已經知道(A[1],……A[N-1])中和最大的子數組和爲MaxSum[1],並且知道,(A[1],……A[N-1])中包含A[1]的和最大的子數組爲TempMaxSum[1]。我們就可以把(A[0],……A[N-1])求和最大子數組問題轉換爲,MaxSum[0] = max{A[0],A[0]+TempMaxSum[1],MaxSum[1]}。

代碼實現:

  1. int MaxSubStringSum(int *A,int n)  
  2. {  
  3.     int MaxSum = A[0];    
  4.     int TempMaxSum = A[0];  
  5.     for(int i = 1;i < n;i++)  
  6.     {  
  7.         TempMaxSum = max(A[i],TempMaxSum + A[i]);  
  8.         MaxSum = max(MaxSum,TempMaxSum);  
  9.     }  
  10.     return MaxSum;  
  11. }  
int MaxSubStringSum(int *A,int n)
{
    int MaxSum = A[0];  
    int TempMaxSum = A[0];
    for(int i = 1;i < n;i++)
    {
        TempMaxSum = max(A[i],TempMaxSum + A[i]);
        MaxSum = max(MaxSum,TempMaxSum);
    }
    return MaxSum;
}
未完待續……




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