《算法导论》学习笔记——背包问题

时间:2023-01-05 18:43:06

一、背包问题(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):

最优子结构:对一个问题来说,如果他的最优解包含子问题的最优解,那该问题就就具有最优子结构性质。

贪心选择性质:一个全局最优解可以通过局部最优(贪心)选择来得到。

注意:贪心算法和动态规划对比。(详见《算法导论》)