1. 程式人生 > >最詳細動態規劃解析——揹包問題

最詳細動態規劃解析——揹包問題

動態規劃的定義

要解決一個複雜的問題,可以考慮先解決其子問題。這便是典型的遞迴思想,比如最著名的斐波那契數列,講遞迴必舉的例子。

斐波納契數列的定義如下:F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
用遞迴可以很快寫出這樣一個函式,咋一看真牛逼,幾行程式碼就搞定了

int fib(int i)
{
    if(i <= 1)
    {
        return 0;
    }
    return fib(i - 1) + fib(i - 2);
}

將該函式的執行過程用圖表示出來,就會發現fib4執行了一次,fib3執行了兩次,fib2執行了三次,fib1計算了5次,重複的次數呈現爆發增長,接近與指數級。如果n取得足夠大,暫且不說費時的問題,直接就會因為遞迴次數太多,函式堆疊溢位而程式奔潰。
image

那麼很快就有人想到,用一個數組來儲存曾經計算過的資料來避免重複計算。這種思想便是動態規劃
我們來實現一下

#include <iostream>
#include <stdlib.h>
#include <vector>

using namespace std;

int fib(int n, vector<int>& vec);

int main(int argc, char** argv)
{
        if(argc != 2)
        {
                cout << "usage: ./a.out number"
<< endl; } int num = atoi(argv[1]); vector<int> vec(num + 1, -1); int ret = fib(num, vec); cout << "fib(" << argv[1] << ")" << " = " << ret << endl; return 0; } int fib(int n, vector<int>& vec) { if
(n <= 1) { return 1; } if(vec[n] == -1) { vec[n] = fib(n - 1, vec) + fib(n - 2, vec); } return vec[n]; }

當然,對於遞迴問題也可以轉化為迴圈來解決。

#include <iostream>
#include <stdlib.h>

using namespace std;

int fib(int n);

int main(int argc, char** argv)
{
        if(argc != 2)
        {
                cout << "usage: ./a.out number" << endl;
        }

        int ret = fib(atoi(argv[1]));
        cout << "fib(" << argv[1] << ")" << " = " << ret << endl;
        return 0;
}


int fib(int n)
{
        if(n <= 1)
        {
                return 1;
        }

        int n1 = 1;
        int n2 = 1;

        for(int i = 1; i < n; ++i)
        {
                int temp = n1;
                n1 = n1 + n2;
                n2 = temp;
        }

        return n1;
}

揹包問題

現在我們來看一個複雜的問題,講動態規劃必須談到的揹包問題,如果理解了此方法,那麼對於同一型別的問題都可以用類似的方法來解決,學演算法最重要的是學會舉一反三。揹包問題分為01揹包問題和完全揹包問題,揹包問題用知乎某答主的話講就是:一個小偷背了一個揹包潛進了金店,包就那麼大,他如果保證他背出來所有物品加起來的價值最大。

01揹包問題的描述:有編號分別為a,b,c,d,e的五件物品,它們的重量分別是2,2,6,5,4,它們的價值分別是6,3,5,4,6,現在給你個承重為10的揹包,如何讓揹包裡裝入的物品具有最大的價值總和?

要說明這個問題,要先了解一下揹包問題的狀態轉換方程: f[i,j] = Max{ f[i-1,j-Wi]+Pi( j >= Wi ), f[i-1,j] }

其中:
f[i,j]表示在前i件物品中選擇若干件放在承重為 j 的揹包中,可以取得的最大價值。
Pi表示第i件物品的價值。

初學者最不懂的地方可能就是這個狀態方程了,i是什麼鬼,j又是什麼鬼?下面具體來說這個狀態方程怎麼來的。
之前說過動態規劃是考慮遞迴的思想,要解決這個問題,首先想到解決其子問題。
要從5箇中選出若干個裝入容量為10的揹包,可以分解為,將a物品裝入揹包,然後從其他四個中選出若干個裝入剩餘容量為8的袋子,因為a已經佔去了2個位置;或者不裝a,從其他四個中選出若干個裝入容量為10的袋子?這兩種做法中,價值最大的就是我們需要的方案。如果選擇了第一種方案,那麼繼續分解,將b物品裝入袋子,從其餘三個中選出若干個裝入剩餘容量為6的袋子,或者不裝b(也許你更樂意裝b),從剩餘三個中選出若干個裝入剩餘容量為8的袋子,選擇這兩種方案中價值最大的。依次類推,直到五個物品都選擇完畢。將其一般化,用i代替a,用j代替10,用數學公式表達出來就是上面那個公式了,是不是覺得已經看懂了這個公式。
上面公式中還有個( j >= Wi ),表示剩餘的容量至少要大於該物品的重量,才需要討論裝不裝的問題。
既然子問題已經解決,那麼自然想到用遞迴了,我們用遞迴來實現

#include <iostream>
#include <vector>

using namespace std;

vector<char> things = {'a', 'b', 'c', 'd', 'e'};
vector<int> value = {6, 3, 5, 4, 6};
vector<int> weight = {2, 2, 6, 5, 4};

int backpack(int n, int w)
{
    if(n == 0 | w == 0)
    {
        return 0;
    }

    int ret;

    if(w < weight[5 - n])
    {
        ret = backpack(n - 1, w);
        cout << "n = " << n << "   w = " << w << "   val = " << ret << endl;
        return ret;
    }

    //n表示從多少件物品中選
    //剛開始可以從五件物品中選,然後就是兩種情況,放入第一件還是不放入第一件
    //第一件選擇完畢後,就需要從其餘四件中選擇,重複上面的過程
    //
    //當n=5時,5-n表示第一件,n=4時候,5-n表示第二件
    int val1 = backpack(n - 1, w - weight[5 - n]) + value[5 - n];
    int val2 = backpack(n - 1, w);

    if(val1 > val2)
    {
        ret = val1;
        //cout << "選擇物品" << things[5 - n] << endl;
    }
    else if(val1 < val2)
    {
        ret = val2;
        //cout << "不選擇物品" << things[5 - n] << endl;
    }
    else
    {
        ret = val1;
        //cout << "拿不拿" << things[5 - n] << "一樣" << endl;
    }

    cout << "n = " << n << "   w = " << w << "   val = " << ret << endl;
    return ret;
}

int main()
{
    int ret = backpack(5, 10);
    cout << "max value = " << ret << endl;
    return 0;
}
//輸出結果
n = 1   w = 1   val = 0
n = 1   w = 6   val = 6
n = 2   w = 6   val = 6
n = 3   w = 6   val = 6
n = 1   w = 2   val = 0
n = 2   w = 2   val = 0
n = 1   w = 3   val = 0
n = 1   w = 8   val = 6
n = 2   w = 8   val = 6
n = 3   w = 8   val = 6
n = 4   w = 8   val = 9
n = 1   w = 2   val = 0
n = 2   w = 2   val = 0
n = 1   w = 3   val = 0
n = 1   w = 8   val = 6
n = 2   w = 8   val = 6
n = 3   w = 8   val = 6
n = 1   w = 4   val = 6
n = 2   w = 4   val = 6
n = 1   w = 5   val = 6
n = 1   w = 10   val = 6
n = 2   w = 10   val = 10
n = 3   w = 10   val = 11
n = 4   w = 10   val = 11
n = 5   w = 10   val = 15
max value = 15

遞迴的過程是怎樣的呢?
這裡寫圖片描述

同樣出現與求斐波那契數列相同的問題,有重複計算的地方。同樣的,採取用陣列來儲存結果,這個結果就是上面那個表,顯然我們要用一個二維陣列才能完成該工作。可以採取,與之前相同的方法,在遞迴里加陣列,但是這次我們換一種方式,用迴圈來做。

對於用迴圈來解文獻2採用的列表方法非常有助於理解,因此我們採用其方法來講述,不同的是我們會將這個表生成的過程進行詳細闡述。下面這個表就是文獻2中用來講述揹包問題的表,大家可以先考慮一下這個表示怎麼生成的。
這裡寫圖片描述

為了便於描述,用e2單元格表示e行2列的單元格,這個單元格的意義是用來表示只有物品e可以選擇了,有個承重為2的揹包,那麼這個揹包的最大價值是0,因為e物品的重量是4,揹包裝不了。對於d2單元格,表示只有物品e,d可以選擇時,承重為2的揹包,所能裝入的最大價值,仍然是0,因為物品e,d都不是這個揹包能裝的。所以這個表列上的數字表示揹包目前的容量,該行以及該行以下的物品是可以選擇的,而該行以上的物品則不是該行可以選擇的。這個表是從下往上、從左往右生成的。

以第4列為例分析一下生成過程:e4用公式表示就是f[1, 4] = max{(f[0, 4 - 4] + 6), f[0, 4]},對於d4用公式表示就是f[2, 4] = max{f[1, 4]}(因為容量為4的揹包裝不下重量為5的d物體),同理c4=f[3, 4]=max{f[2, 4]},b4 = f[4, 4] = max{(f[3, 4 - 2] + 3), f[3, 4]}

/************************************************************************/
/* 01揹包問題
** 問題描述:有編號分別為a,b,c,d,e的五件物品,它們的重量分別是2,2,6,5,4,它們的價值分別是6,3,5,4,6,現在給你個承重為10的揹包,如何讓揹包裡裝入的物品具有最大的價值總和?
/************************************************************************/
#include <tchar.h>
#include <iostream>
#include <vector>
#include <string.h>
#include <cstdlib>

using namespace std;

int weight[5] = {2, 2, 6, 5, 4};   //每個物品的重量
int value[5] = {6, 3, 5, 4, 6};      //每個物品的價值
int C[6][11];   //儲存各種情況能裝下物品價值的陣列

vector<int> path;

void FindAnswer()
{
    int capacity = 10;
    for (int i = 5; i > 0; --i)
    {
        if (C[i][capacity] > C[i - 1][capacity])
        {
            path.push_back(i);
            capacity -= weight[i - 1];
        }
    }
}

void Package()
{
    for (int i = 0; i < 11; i++)
    {
        for (int j = 0; j <6; ++j)
        {
            if (i == 0)
            {
                //可選物品為0,所以能裝的價值只能為0
                C[j][i] = 0;
            }
            else if (j == 0)
            {
                //容量為零,所以能裝的價值也是0
                C[j][i] = 0;
            }
            else
            {
                //判斷當前容量能放入
                if (i >= weight[j - 1])
                {
                    C[j][i] =  max(C[j - 1][i], (C[j -1][i - weight[j - 1]] + value[j - 1]) );
                }
                //如果不能放入,則不放入該物品
                else
                {
                    C[j][i] = C[j - 1][i];
                }
            }           
        }
    }
}

int _tmain(int args, TCHAR* argv[])
{
    memset(C, -1, sizeof(C));
    Package();
    FindAnswer();
    return 0;
}

舉一反三

參考文獻: