不妨先用最樸素的方法,針對每個物品是否放入揹包進行搜索:
// 輸入
int n, W;
int w[MAX_N], v[MAX_N];
// 從第 i 個物品開始挑選總重小於 j 的部分
int rec(int i, int j) {
int res;
if (i == n) {
// 已經沒有剩餘物品了
res = 0;
} else if (j < w[i]) {
// 無法挑選這個物品
res = rec(i + 1, j);
} else {
// 挑選和不挑選的兩種情況都嘗試一下
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
return res;
}
void solve() {
printf("%d\n", rec(0, W));
}
只不過,這種方法的搜索深度是 ,而且每一層的搜索都需要兩次分支,最壞的就需要 的時間,當 比較大時就沒辦法解了。由於 在遞歸調用時存在重複調用的情況,所以我們可以把第一次計算時的結果記錄下來,省略掉第二次以後的重複計算試試看。
int dp[MAX_N + 1][MAX_W + 1]; // 記憶化數組
int rec(int i, int j) {
if (dp[i][j] >= 0) {
// 已經計算過的話直接使用之前的結果
return dp[i][j];
}
int res;
if (i == n) {
res = 0;
} else if (j < w[i]) {
res = rec(i + 1, j);
} else {
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
}
// 將結果儲存在數組中
return dp[i][j] = res;
}
void solve() {
// 用 -1 表示尚未計算過,初始化整個數組
memset(dp, -1, sizeof(dp));
printf("%d\n", rec(0, W));
}
對於同樣的參數,只會在第一次被調用到時執行遞歸部分,第二次之後都會直接返回。參數的組合不過 種,而函數內只調用兩次遞歸,所以只需要 的複雜度就能解決問題。這種方法一般被稱爲記憶化搜索。
如果對記憶化搜索還不是很熟練的話,可以寫成窮竭搜索的寫法:
// 目前選擇的物品價值總和是 sum,從第 i 個物品之後的物品中挑選重量總和小於 j 的物品
int rec(int i, int j, int sum) {
int res;
if (i == n) {
// 已經沒有剩餘物品了
res = sum;
} else if (j < w[i]) {
// 無法挑選這個物品
res = rec(i + 1, j, sum);
} else {
// 挑選和不挑選的兩種情況都嘗試一下
res = max(rec(i + 1, j, sum), rec(i + 1, j - w[i], sum + v[i]));
}
return res;
}
在需要剪枝的情況下,可能會像這樣把各種參數都寫在函數上,但在這種下會讓記憶化搜索難以實現,需要注意。
接下來,我們來仔細研究一下前面的算法利用到的這個記憶化數組。記 爲根據 的定義,從第 個物品開始挑選總重小於 時,總價值的最大值。於是就有如下遞推式:
這樣不用寫遞歸函數,直接利用遞推式將各項的值計算出來,簡單的二重循環也能解決這一問題。
int dp[MAX_N + 1][MAX_W + 1]; // DP 數組
void solve() {
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[i][j] = dp[i + 1][j];
} else {
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
}
}
}
printf("%d\n", dp[0][W]);
}
這個算法的複雜度與前面的相同,也是 ,但是簡潔了很多。以這種方式一步步按順序求出問題的解的方法被稱作動態規劃法,也就是常說的 。