一、揹包問題(knapsack problem)
(參考維基百科:
http://en.wikipedia.org/wiki/Knapsack_problem)
1. 0-1 揹包問題(0-1 knapsack problem the most common problem):
2. 有界揹包問題(bounded knapsack problem BKP):
3. 無界揹包問題(unbounded knapsack problem UKP):
二、0-1揹包問題
特點:每件物品或被帶走,或被留下(需要做出0-1選擇)。不能只帶走某個物品的一部分或帶走兩次以上同一個物品。
輸入:數組v,數組w,可取走的物品數n,可取走的最大重量W。
輸出(可選):數組x,x中每個元素爲1或0,1對應該物品被取走,0對應不取;滿足條件下揹包的總重量及總價值。
分析:一個物品選或不選,共有2^n中組合方式,目的就是找到這些組合方式中滿足條件的一種。
解法一 暴力破解法
分析:枚舉2^n種組合方式下的總重量和總價格,找到滿足條件的最優解。
時間複雜度:O(lg1 + lg2 + lg3 + ... + lg(2^n)) = O(lg((2^n)!)) > O(2^n)。
算法C實現:
枚舉時採用位運算,在每一位上通過判斷0或1指示該位表示的物品是否取走。
void knapsack_problem_violent(ElementType* v, ElementType* w,
CommonType count,
ElementType maximum_weight,
ElementType* best_weight,
ElementType* best_value,
ElementType* x)
{
assert(v != NULL && w != NULL && count >= 1 && maximum_weight > 0 &&
x != NULL && best_value != NULL && best_weight != NULL);
/// initialize
CommonType i, j, k;
ElementType temp_weight, temp_value;
CommonType x_temp[MAX_COUNT] = {0};
*best_weight = 0; *best_value = 0;
memset(x, 0, count * sizeof(ElementType));
/// detect all combination
CommonType type_count = pow(2, count);
for (i = 0; i < type_count; i++) {
/// get current total weight and totoal value and combination
memset(x_temp, 0, count * sizeof(ElementType));
temp_value = 0;
temp_weight = 0;
k = 0;
j = i;
do {
/// 1 bit correspond to 1 item
if (j & 0x1) {
temp_value += v[k];
temp_weight += w[k];
x_temp[k] = 1;
}
k++;
} while (j >>= 1);
/// save best state: best weight, best value and item state
if (temp_weight < maximum_weight && temp_value > *best_value) {
*best_weight = temp_weight;
*best_value = temp_value;
memcpy(x, x_temp, count * sizeof(ElementType));
}
}
}
解法二 遞歸算法 分治策略
遞歸的子問題定義詳見解法三,這裏只給出算法C實現:
ElementType knapsack_problem(ElementType* v, ElementType* w,
CommonType count,
ElementType maximum_weight)
{
assert(v != NULL && w != NULL && count >= -1 && maximum_weight >= 0);
/// no items or no weight
if (count == -1 || maximum_weight == 0)
return 0;
/// the i-th item's weight exceed the range
if (w[count - 1] > maximum_weight)
return knapsack_problem(v, w, count - 1, maximum_weight);
/// get best solution
ElementType a = knapsack_problem(v, w, count - 1, maximum_weight);
ElementType b = knapsack_problem(v, w, count - 1,
maximum_weight - w[count - 1]) + v[count - 1];
return a > b ? a : b;
}
解法三 動態規劃 dynamic programming
動態規劃分析:
最優子結構 Optimal Structure:即滿足一個問題的最優解包含子問題最優解的情況。選擇一個給定物品i,則需要比較選擇i的形成的子問題的最優解與不選擇i的形成的子問題的最優解。問題分成兩個子問題,進行選擇比較,選擇最優的一種。
重疊子問題 Overlapping subproblems:子問題之間並非獨立,存在重疊情況。
難點:把問題抽象化並建立數學模型,用科學的數學語言描述問題與子問題的關係,找到狀態轉移點並推出遞歸關係,以便編程求解。
問題:在N個物品中選擇,在揹包最大重量W約束下揹包所能達到的最大價值。
該問題子問題可分爲兩個:
子問題1:不選擇第N個物品,在前N-1個物品中選擇,在揹包最大重量W約束下揹包所能達到的最大價值可能爲問題最優解。
子問題2:選擇第N個物品(重量爲w,價值爲v),並在前N-1個物品中選擇,在揹包最大重量W-w約束下揹包所能達到的最大價值加上第N個物品的價值v可能爲問題最優解。
這樣問題最優解就可以轉化求解相應兩種子問題的最優解。數學表達如下:
定義v(i,w)爲在前i個物品中選擇取捨(並不是說前i個物品都要取),並且揹包最大重量爲w時揹包所能達到的最大價值(最優解)。根據題意可得0<=i<=n,0<=w<=W。
可以得到遞歸關係:
v(i,w) = max(v(i-1,w),v(i-1,w-wi) + vi) (其中wi,vi爲第i個物品重量和價值,v(i-1,w)對應不選擇第i個物品時最優解,v(i-1,w-wi)+vi對應選擇第i個物品時最優解)
v(i,w) = 0, 此時i = 0 或 w = 0。
v(i,w) = v(i-1,w), 此時wi > w,避免揹包最大重量爲負數。
時間複雜度:O(n*W)。
算法C實現:
動態規劃採用自底向上法(bottom-up method);
keep數組用於尋找哪些物品被放入揹包哪些物品被舍,keep(i,w) = 1表示v(i,w)這一最優解情況下保留第i個物品,keep(i,w) = 0時表示不保留。利用keep數組,可進行回溯(trace back),從而判斷v(n,W)情況下哪些物品放入揹包,哪些物品被捨去;
V數組和keep數組第一列和第一行分別表示揹包最大重量w = 0和可選擇物品數爲i = 0時問題的解,爲特殊解,需要初始化爲0(邊界問題)。
/**
* @file knapsack_problem_dp_bottom_up.c
* @brief solve 0-1 knapsack problem by dynamic programming
* with bottom-up method.
* @author chenxilinsidney
* @version 1.0
* @date 2015-03-04
*/
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <math.h>
// #define NDEBUG
#include <assert.h>
// #define NDBG_PRINT
#include "debug_print.h"
typedef int ElementType; ///< element data type
typedef int CommonType; ///< common data type
/// data
#define MAX_COUNT 100 ///< count of the items
#define MAX_WEIGHT 300 ///< max weight of the items for memory
ElementType v[MAX_COUNT + 1] = {0};
ElementType w[MAX_COUNT + 1] = {0};
CommonType x[MAX_COUNT + 1] = {0};
CommonType V[MAX_COUNT + 1][MAX_WEIGHT + 1] = {{0}};
CommonType keep[MAX_COUNT + 1][MAX_WEIGHT + 1] = {{0}};
ElementType knapsack_problem(ElementType* v, ElementType* w,
ElementType* x,
CommonType count,
ElementType maximum_weight,
ElementType V[MAX_COUNT + 1][MAX_WEIGHT + 1],
ElementType keep[MAX_COUNT + 1][MAX_WEIGHT + 1])
{
assert(v != NULL && w != NULL && count >= -1 && maximum_weight >= 0 &&
V != NULL);
CommonType i, j;
/// initialize first row
for (i = 0; i <= maximum_weight; i++) {
V[0][i] = 0;
keep[0][i] = 0;
}
for (i = 1; i <= count; i++) {
/// initialize first column
V[i][0] = 0;
keep[i][0] = 0;
/// get matrix solution
for (j = 1; j <= maximum_weight; j++) {
if(w[i - 1] <= j) {
ElementType a = V[i - 1][j];
ElementType b = V[i - 1][j - w[i - 1]] + v[i - 1];
V[i][j] = a > b ? a : b;
if (a > b)
keep[i][j] = 0;
else
keep[i][j] = 1;
} else {
V[i][j] = V[i - 1][j];
keep[i][j] = 0;
}
}
}
/// display the matrix
printf("-V--");
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", j);
printf("\n");
for (i = 0; i <= count; i++) {
printf("%3d ", i);
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", V[i][j]);
printf("\n");
}
printf("keep");
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", j);
printf("\n");
for (i = 0; i <= count; i++) {
printf("%3d ", i);
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", keep[i][j]);
printf("\n");
}
/// get and display the item list by keep matrix
printf("k = ");
ElementType K = maximum_weight;
for (i = count; i >= 1; i--) {
if (keep[i][K] == 1) {
printf("%3d ", i);
K -= w[i - 1];
x[i - 1] = 1;
} else {
x[i - 1] = 0;
}
}
printf("\n");
printf("actual weight = %3d\n", maximum_weight - K);
return V[count][maximum_weight];
}
int main(void) {
/// read data to array
/// read maximum weight
ElementType maximum_weight = 0;
if (scanf("%d\n", &maximum_weight) != 1 || maximum_weight <= 0) {
DEBUG_PRINT_STATE;
DEBUG_PRINT_STRING("can not get right maximum_weight");
}
CommonType count = 0;
while(count < MAX_COUNT && scanf("(%u,%u)\n", v + count, w + count) == 2) {
++count;
}
/// get result
ElementType best_value = knapsack_problem(v, w, x, count,
maximum_weight, V, keep);
/// output result
printf("best value = %d\n", best_value);
CommonType i;
for (i = 0; i < count; i++) {
printf("item index = %3d, value = %3d, weight = %3d, get_flag = %3d\n",
i + 1, v[i], w[i], x[i]);
}
return EXIT_SUCCESS;
}
輸入數據:
10
(10,5)
(40,4)
(30,6)
(50,3)
輸出數據:
-V-- 0 1 2 3 4 5 6 7 8 9 10
0 0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 10 10 10 10 10 10
2 0 0 0 0 40 40 40 40 40 50 50
3 0 0 0 0 40 40 40 40 40 50 70
4 0 0 0 50 50 50 50 90 90 90 90
keep 0 1 2 3 4 5 6 7 8 9 10
0 0 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 1 1 1 1 1 1
2 0 0 0 0 1 1 1 1 1 1 1
3 0 0 0 0 0 0 0 0 0 0 1
4 0 0 0 1 1 1 1 1 1 1 1
k = 4 2
actual weight = 7
best value = 90
item index = 1, value = 10, weight = 5, get_flag = 0
item index = 2, value = 40, weight = 4, get_flag = 1
item index = 3, value = 30, weight = 6, get_flag = 0
item index = 4, value = 50, weight = 3, get_flag = 1
優化:
1.內存空間優化(減少空間複雜度),把v從二維數組v(i,w)降爲一維數組v(w),理由:子問題只需要一維方向上的信息,考慮到更新一維數組是否對後續問題求解帶來影響,採用遞減方式進行以防止舊有數據被更新而無法使用;
2.可選優化(未實現):可以在遍歷過程中隨着i增加而讀入相應物品的數據vi和wi,避免開闢兩個長度數組存儲這些物品數據。
優化部分代碼:
ElementType knapsack_problem(ElementType* v, ElementType* w,
ElementType* x,
CommonType count,
ElementType maximum_weight,
ElementType V[MAX_COUNT + 1],
ElementType keep[MAX_COUNT + 1][MAX_WEIGHT + 1])
{
assert(v != NULL && w != NULL && count >= -1 && maximum_weight >= 0 &&
V != NULL);
CommonType i, j;
/// initialize first row
for (i = 0; i <= maximum_weight; i++) {
keep[0][i] = 0;
}
for (i = 1; i <= count; i++) {
/// initialize first column
V[0] = 0;
keep[i][0] = 0;
/// get matrix solution
for (j = maximum_weight; j >= 1; j--) {
if(w[i - 1] <= j) {
ElementType b = V[j - w[i - 1]] + v[i - 1];
if (V[j] < b) V[j] = b;
if (V[j] > b)
keep[i][j] = 0;
else
keep[i][j] = 1;
} else {
keep[i][j] = 0;
}
}
}
/// display the matrix
printf("-V--");
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", j);
printf("\n");
for (i = 0; i <= count; i++) {
printf("%3d ", i);
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", V[j]);
printf("\n");
}
printf("keep");
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", j);
printf("\n");
for (i = 0; i <= count; i++) {
printf("%3d ", i);
for (j = 0; j <= maximum_weight; j++)
printf("%3d ", keep[i][j]);
printf("\n");
}
/// get and display the item list by keep matrix
printf("remain weight = ");
ElementType remain_weight = maximum_weight;
for (i = count; i >= 1; i--) {
if (keep[i][remain_weight] == 1) {
printf("%3d ", i);
remain_weight -= w[i - 1];
x[i - 1] = 1;
} else {
x[i - 1] = 0;
}
}
printf("\n");
printf("actual weight = %3d\n", maximum_weight - remain_weight);
return V[maximum_weight];
}
其他:採用動態規劃解決0-1揹包問題,貪心算法對0-1揹包問題無效。
三、分數揹包問題(factional knapsack problem):
與0-1揹包問題區別:可以那種物體的一部分,而不是隻能做出二元(0-1)選擇。
使用貪心算法(greedy algorithm),我們計算每個物品單位重量價值,遵循貪心策略,首先儘量多地拿走物品單位重量價值最高的物品,以此類推,直到達到上限W。
時間複雜度:由於需要進行對物品的單位重量價值進行排序,時間複雜度爲O(nlgn)。
貪心算法(greedy algorithm):
最優子結構:對一個問題來說,如果他的最優解包含子問題的最優解,那該問題就就具有最優子結構性質。
貪心選擇性質:一個全局最優解可以通過局部最優(貪心)選擇來得到。
注意:貪心算法和動態規劃對比。(詳見《算法導論》)