变种 背包问题_算法导论学习笔记(八):背包问题

转载请注明原作者 @yoshino,强烈要求简书支持TOC目录和数学公式的输入!

更新不及时,有空慢慢写吧,原文是写在Bear上粘贴来简书的,可能格式有点乱。

具体的工程在我的github上。

一个正在抢劫商店的小偷发现了n个商品,第i个商品的价值vi元,重wi磅,小偷希望拿走价值尽量高的商品,但是他的背包只能容纳W磅商品,怎么选择才能拿走价值最大的商品?

根据算法导论书上原题和自定一些条件,把这题条件定义如下:

int[] n = new int[]{0, 3, 3, 3};//为了配合算法,下标都是从1开始,表示每件商品的数量

int[] w = new int[]{0, 10, 20, 30};//表示每件商品的重量

int[] v = new int[]{0, 60, 100, 120};//表示每件商品的价值

int W = 50;//背包的容积

背包问题分为两种:

0-1背包问题(0-1 knapsack problem):商品不可分,小偷只能完整拿走或者不拿走,或者同一个商品拿走多次

分数背包问题(fractional knapsack problem):商品可分,小偷可以拿走商品的部分

问题分析

分数背包问题

分数背包问题是典型的贪心算法,只要计算出商品的每磅价值vi/wi,贪心地选择每磅价值最高的商品即可

public void fracKnapsack(int[] n, int[] w, int[] v, int W) {

int[] value = new int[n.length];//计算每件商品的单磅价值大小

int[] result = new int[n.length];//存储取用结果

value[0] = 0;

result[0] = 0;

for (int i = 1; i < n.length; i++) {

value[i] = v[i] / w[i];//计算每磅价值

n[i] = n[i] * w[i];//计算每件商品一共有多少磅

result[i] = 0;

}

while (W > 0) {

int pos = getMax(value);

if (n[pos] > 0) {

while (n[pos] > 0) {

result[pos]++;

n[pos]--;

W--;

}

}

if (n[pos] == 0) {

value[pos] = 0;

continue;

}

}

int sum = 0;

System.out.println("拿走物品结果:");

for (int i = 1; i < result.length; i++) {

System.out.println("物品编号:" + i + " 数量:" + result[i] / w[i] + " 单价:" + v[i]);

sum = sum + (result[i] * v[i] / w[i]);

}

System.out.println("总价值为: " + sum);

}

private int getMax(int[] value) {//找出单磅价值最大的商品编号

int pos = 0;

int max = 0;

for (int i = 1; i < value.length; i++) {

if (value[i] != 0) {

if (max < value[i]) {

max = value[i];

pos = i;

}

}

}

return pos;

}

0-1背包问题

0-1背包问题不能用贪心算法来解决,原因很简单,使用贪心算法会留下空间,无法完全填满背包,浪费的空间会造成单磅价值的下降,所以必须采用动态规划。

0-1背包问题是动态规划中最基础的问题,后面还会有各种背包问题,这仅仅是0-1背包问题的变种.

我们假设f(i,W)是,有i个商品,背包容积为W时的最优解,对于一个新的商品ai,有两种不同的情况:

ai的重量wi大于背包容积,则无法将其放入背包,商品的数量变为i+1

ai的重量wi小于等于W,则将其放入背包,剩下的背包容积为W-wi,或者不放,取两者之间较大的值。

状态转移方程可以写成:

f[i][W]=max{f[i-1][W],f[i-1][W-w[i]]+v[i]}

最优子结构可以表示成下面公式

...

\\对应的latex公式代码,不支持latex真麻烦

$$

f(i,W)=

\begin{cases}

f(i-1,W)&&,w_i>W\\

max[f(i-1,W),&f(i-1,W-w_i)+v_i]&,w_i\le W

\end{cases}

$$

public void zeroOneKnapsack(int[] n, int[] w, int[] v, int W) {//带备忘的自顶向下法

int[][] result = new int[n.length][W + 1];//建立最优解矩阵,result[有前i种商品][背包容量W]

for (int i = 0; i <= W; i++) {

result[0][i] = 0;//初始化

}

for (int i = 1; i < n.length; i++) {//商品i种,从1开始

for (int j = 0; j <= W; j++) {//容量大小,从0到W

if (w[i] > j) {//无法放入背包,根据最优子结构可知

result[i][j] = result[i - 1][j];

} else if (w[i] <= j) {//放入背包中

int maxProfit = 0;

maxProfit = Math.max(result[i - 1][j], result[i - 1][j - w[i]] + v[i]);//选择放还是不放

result[i][j] = maxProfit;//存储最优解

}

}

}

System.out.println(result[n.length - 1][W]);

}

其它背包问题

完全背包问题

相比于0-1背包问题,每件物品的个数已经变成无限个。

求解思路

我们可以转化为0-1背包问题来求解,将k件一种物品拆成k种价值一样的物品,这样就可以采用上述代码写了。但是这样写增大了空间复杂度,也没有改进时间复杂度。

还有一种方法是转化成二进制思想,不太清楚如何写的,其实思想也是分成0-1背包问题来解决的

多重背包问题

介于完全背包问题和0-1背包问题之间,第i种物品有n[i]件,其余条件不变

求解思路

同样的,可以拆解成0-1背包问题,这样的话就大大地增加空间复杂度了

结语

通过对背包问题的学习,基本上困难的算法问题到最后都是可以求解成最简单的0-1背包问题,基础还是很重要。

你可能感兴趣的:(变种,背包问题)