最詳細動態規劃解析——揹包問題
動態規劃的定義
要解決一個複雜的問題,可以考慮先解決其子問題。這便是典型的遞迴思想,比如最著名的斐波那契數列,講遞迴必舉的例子。
斐波納契數列的定義如下: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取得足夠大,暫且不說費時的問題,直接就會因為遞迴次數太多,函式堆疊溢位而程式奔潰。
那麼很快就有人想到,用一個數組來儲存曾經計算過的資料來避免重複計算。這種思想便是動態規劃!
我們來實現一下
#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;
}