完全背包

描述: 有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。第 i 种物 品的费用是 c[i],价值是 w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且 价值总和最大。

分析:完全背包和01背包的区别就是物品的数量,01背包的结果对于一个物品来说是要么有0个要么有1个(所以称之为01背包),而完全背包,不仅需要考虑有没有,还需要考虑有多少个。只需要对01背包的代码进行简单改造,就可以解决这样的问题。

每个物品有无限个

import java.util.*;public class BackPackUtil {static class CompletelyResult {Thing thing;Integer cnt;}public static Map<Integer,Set<CompletelyResult>> backPackCompletely(List<Thing> things, Integer totalCost) {Map<Integer, Set<CompletelyResult>> ans = new HashMap<>();int n = things.size();int[] f = new int[totalCost + 1];f[0] = 0;for (int i = 0; i < n; i++) {Thing thing = things.get(i);int curWeight = thing.getWeight();int curIncome = thing.getIncome();for (int j = thing.getWeight(); j <= totalCost; j++) {if ((f[j - curWeight] + curIncome) > f[j]) {if (!ans.containsKey(j)) {ans.put(j, new HashSet<>());}Set<CompletelyResult> completelyResults = new HashSet<>();for (CompletelyResult completelyResult : ans.get(j)) {CompletelyResult completelyResult1 = new CompletelyResult();completelyResult1.thing = completelyResult.thing;completelyResult1.cnt = completelyResult.cnt;completelyResults.add(completelyResult1);}if (!ans.containsKey(j - curWeight)) {ans.put(j - curWeight, new HashSet<>());}ans.get(j).clear();for (CompletelyResult completelyResult : ans.get(j - curWeight)) {CompletelyResult completelyResult1 = new CompletelyResult();completelyResult1.thing = completelyResult.thing;completelyResult1.cnt = completelyResult.cnt;ans.get(j).add(completelyResult1);}int flag = 0;for (CompletelyResult completelyResult : ans.get(j)) {if (completelyResult.thing.getName().equals(thing.getName())) {completelyResult.cnt += 1;flag = 1;break;}}if (flag == 0) {f[j] = f[j - curWeight] + curIncome;CompletelyResult result = new CompletelyResult();result.thing = thing;result.cnt = 1;ans.get(j).add(result);}if (flag == 1) {f[j] = f[j - curWeight] + curIncome;}}}}return ans;}public static void print(Set<CompletelyResult> solveMethod,Integer total){StringBuffer thingss = new StringBuffer();int totalValue = 0;int totalWeight = 0;for (CompletelyResult result : solveMethod) {Thing thing = result.thing;thingss.append(thing.getName()).append(" ").append(result.cnt).append("个 ").append(thing.getWeight() * result.cnt).append(" ").append(result.cnt * thing.getIncome()).append("\n");totalValue = totalValue + thing.getIncome() * result.cnt;totalWeight += thing.getWeight() * result.cnt;}System.out.println("计划成本为:" + total + " 总消耗为:" + totalWeight + " 总收益为:" + totalValue);System.out.print(thingss.toString());System.out.println();}}

测试代码为:

public static void main(String[] args) {String[][] a = {{"01-1", "3", "2", "3"}, {"01-2", "10", "4", "7"},{"01-3", "20", "8", "15"}, {"01-4", "5", "16", "31"}, {"01-5", "3", "25", "51"}};List<Thing> things = new ArrayList<>();for (String[] b : a) {Thing thing = new Thing();thing.setName(b[0]);thing.setNumber(Integer.parseInt(b[1]));thing.setWeight(Integer.parseInt(b[2]));thing.setIncome(Integer.parseInt(b[3]));things.add(thing);}Map<Integer,Set<CompletelyResult>> solveMethod = backPackCompletely(things, 300);for (int i = 100; i < 300; i++) {print(solveMethod.get(i),i);}}

结果如下:
执行结果为:

代码分析

这里的核心在于

for (int i = 0; i < n; i++) {Thing thing = things.get(i);int curWeight = thing.getWeight();int curIncome = thing.getIncome();for (int j = thing.getWeight(); j <= totalCost; j++) {if ((f[j - curWeight] + curIncome) > f[j]) {... ...}... ...}... ...
}

第二层for循环j是从成本开始遍历,这与01背包j从总数开始是反的,而这种反的正好是表现在一个物品是否可以被多次选择的问题上。写代码时,可以按照如果是逆向就只能被选一次,正向就能被选多次。
那我们来看看原因:为啥逆向就只能被选一次,正向就能被选多次?

我们简单模拟一下这个循环过程,首先,来看看01背包,

for (int i = 0; i < n; i++) {for (int j = totalCost; j >= things.get(i).getWeight(); j--) {... ...}
}

循环体长这个样子,对于第0个物品来说,有下面的转换过程

f[totalCost] = f[totalCost-weight[i]] + val[i];
f[totalCost-1] = f[totalCost-weight[i]-1] + val[i];
f[totalCost-2] = f[totalCost-weight[i]-2] + val[i];
f[totalCost-3] = f[totalCost-weight[i]-3] + val[i];
f[totalCost-4] = f[totalCost-weight[i]-4] + val[i];
f[totalCost-5] = f[totalCost-weight[i]-5] + val[i];
f[totalCost-6] = f[totalCost-weight[i]-6] + val[i];
f[totalCost-7] = f[totalCost-weight[i]-7] + val[i];

由于我们是从后向前逆推时,前面的都为0。所以本质上来讲,对于首个物品来说,所有在weight ~ totalCost范围内的结果都等于val[i]。因为当我给后者赋值的时候,前者的值必然为0。
然后你就会发现,对于第0个物品,它就不会再在其他循环中用到,只用了一次。然后利用普适性原理,即所有物品都有可能做第一个物品,所以就可以推导出,所有物品都只会用一次。

而对于完全背包,其循环过程可以精简为:

for (int i = 0; i < n; i++) {for (int j = things.get(i).getWeight(); j <= totalCost; j++) {... ...}
}

对于第0个物品来说,有下面的转换过程:

f[w] = f[w - w] + v = f[0] + v;
f[w+1] = f[w + 1 - w] + v = f[1] + v;
f[w+2] = f[2] + v;
f[w+3] = f[3] + v;
f[w+4] = f[4] + v;
... ...

由于不确定w的值,所以f[1]/f[2]/f[3]/f[4]的值也不能确定。当w=1时

f[1] = f[0] + v = v;
f[1 + 1 ] = f[1] + v = v + v;
... ...

也就是说,根据w值不同,有可能对于一种消耗总成本,会出现同种物品出现多次的情况。所以这是完全本报的解法。

多重背包

每个物品是有限个

上面的完全背包,每个物品是无限个。但是在我们日常生活的场景中,物品都是有限的。这就是多重背包。

看似正确的写法

现在需要对代码进行简单改造。我们在原来的循环过程中加入数量判断,如下:

public static Map<Integer,Set<CompletelyResult>> backPackCompletely(List<Thing> things, Integer totalCost) {Map<Integer, Set<CompletelyResult>> ans = new HashMap<>();int n = things.size();int[] f = new int[totalCost + 1];f[0] = 0;for (int i = 0; i < n; i++) {Thing thing = things.get(i);int curWeight = thing.getWeight();int curIncome = thing.getIncome();for (int j = thing.getWeight(); j <= totalCost; j++) {if ((f[j - curWeight] + curIncome) > f[j]) {if (!ans.containsKey(j)) {ans.put(j, new HashSet<>());}Set<CompletelyResult> completelyResults = new HashSet<>();for (CompletelyResult completelyResult : ans.get(j)) {CompletelyResult completelyResult1 = new CompletelyResult();completelyResult1.thing = completelyResult.thing;completelyResult1.cnt = completelyResult.cnt;completelyResults.add(completelyResult1);}if (!ans.containsKey(j - curWeight)) {ans.put(j - curWeight, new HashSet<>());}ans.get(j).clear();for (CompletelyResult completelyResult : ans.get(j - curWeight)) {CompletelyResult completelyResult1 = new CompletelyResult();completelyResult1.thing = completelyResult.thing;completelyResult1.cnt = completelyResult.cnt;ans.get(j).add(completelyResult1);}int flag = 0;for (CompletelyResult completelyResult : ans.get(j)) {if (completelyResult.thing.getName().equals(thing.getName())) {if ((completelyResult.cnt + 1) > completelyResult.thing.getNumber()){flag = 2;break;}else {completelyResult.cnt += 1;flag = 1;break;}}}if (flag == 0) {f[j] = f[j - curWeight] + curIncome;CompletelyResult result = new CompletelyResult();result.thing = thing;result.cnt = 1;ans.get(j).add(result);}if (flag == 1) {f[j] = f[j - curWeight] + curIncome;}if (flag == 2) {ans.get(j).clear();ans.get(j).addAll(completelyResults);}}}}return ans;
}

我们在加入子过程的时候加入了特判,如果子过程加上当前这个物品,就超过可这个物品的上限,就不做处理。这个逻辑看似没有什么问题,实则有一个潜在的坑,如果你没有发现,那就接着看

然后,确定输入:

String[][] a = {{"01-1", "3", "2", "3"}, {"01-2", "10", "4", "7"},{"01-3", "20", "8", "15"}, {"01-4", "5", "16", "31"}, {"01-5", "3", "25", "51"}};
List<Thing> things = new ArrayList<>();
for (String[] b : a) {Thing thing = new Thing();thing.setName(b[0]);thing.setNumber(Integer.parseInt(b[1]));thing.setWeight(Integer.parseInt(b[2]));thing.setIncome(Integer.parseInt(b[3]));things.add(thing);
}

这里定义了 01-1 有3个,01-2有10个,01-3有20个,01-4有5个,01-5有3个。
做打印测试:

Map<Integer,Set<CompletelyResult>> solveMethod = backPackCompletely(things, 211);
for (int i = 180; i <= 211; i++) {if (!print(solveMethod.get(i),i)){System.out.println(i);}
}

其中,print函数如下:

public static boolean print(Set<CompletelyResult> solveMethod,Integer total){StringBuffer thingss = new StringBuffer();int totalValue = 0;int totalWeight = 0;for (CompletelyResult result : solveMethod) {Thing thing = result.thing;thingss.append(thing.getName()).append(" ").append(result.cnt).append("个 ").append(thing.getWeight() * result.cnt).append(" ").append(result.cnt * thing.getIncome()).append("\n");totalValue = totalValue + thing.getIncome() * result.cnt;totalWeight += thing.getWeight() * result.cnt;}System.out.println("计划成本为:" + total + " 总消耗为:" + totalWeight + " 总收益为:" + totalValue);System.out.print(thingss.toString());System.out.println();if ((total-totalWeight)*1.0/total >= 0.35){return false;}return true;
}

让我们看看执行结果:

结果很戏剧性,第1部分,结果是正确的,而第2部分的成本200、201、202反倒结果是错误的,那么为啥会出现这样的问题呢?

当代码执行到后期,程序本身已经找到了完全背包写法的最优解,如果此时强行加入个数的条件判断,那么会产生回退,而由于该写法并没有记录上一个过程,所以虽然状态产生了回滚,但是对于子过程来说,却是最新的状态,而不是上一个状态,而最新状态和当前过程的物品相加,会导致总成本大于预期成本,从而不使用该最优解,而等于该过程原来的状态。所以你会发现,后面不正确的,都没有01-5这类靠后的物品。

那么这个写法只能弃了。

正确的写法

在说理论结果之前,我们来看看01背包、完全背包和多重背包的定义差别

名称 物品数量
01背包 0个或1个
完全背包 0个或无穷个
多重背包 0个或多个

而在数学理论中,有限值和无限值是两个概念,所以可以发现,多重背包的概念相比于完全背包,更接近与01背包。那么其实我们就有一个简单的思路,就是对多重背包进行拆分,对于每个产品i,其有n个,可以拆分为n个01背包里面的物品,名字以原来的为前缀,算法结果完毕之后,再做一次处理即可。

不过,分解成n个性质一样的物品对程序来说,消耗可能会有点大,这里需要引入一个数学概念:每一个数字都可以表示为 1 + 2^1 + 22+…+2k,因为每个数字都有二进制表示法。所以也可以将这n个物品i合并成对于n这个数字二进制位中为1的物品集合。先来定义这个过程。

public static List<Thing> splitThing(Thing thing) {int n = thing.getNumber();String x = Integer.toBinaryString(n);int m = x.length();List<Thing> things = new ArrayList<>();for (int i = 0; i < m; i++) {if (x.charAt(i) == '1') {int multiple = new Double(Math.pow(2, m - i - 1)).intValue();Thing thing1 = new Thing();thing1.setName(thing.getName() + "----" + multiple);thing1.setIncome(thing.getIncome() * multiple);thing1.setWeight(thing.getWeight() * multiple);thing1.setNumber(1);things.add(thing1);}}return things;
}

定义了一个分解物品的方法,如上述过程。有分解就有合并,现在我们需要定义出合并方法

public static Set<Thing> mergeThing(Set<Thing> newThings, Set<Thing> oldThings) {Map<String, Thing> oldThingMaps = new HashMap<>();for (Thing thing : oldThings) {oldThingMaps.put(thing.getName(), thing);}Map<String, Thing> newThingMaps = new HashMap<>();for (Thing thing : newThings) {Thing thing1 = oldThingMaps.get(thing.getName().split("----")[0]);Thing newThing = new Thing();if (newThingMaps.containsKey(thing1.getName())) {newThing = newThingMaps.get(thing1.getName());}int multiple = thing.getWeight() / thing1.getWeight();newThing.setNumber(newThing.getNumber() + multiple);newThing.setWeight(thing1.getWeight());newThing.setIncome(thing1.getIncome());newThing.setName(thing1.getName());newThingMaps.put(newThing.getName(), newThing);}return new HashSet<>(newThingMaps.values());
}

然后,我们直接利用调用01背包算法的方法,来实现多重背包的实现,代码如下:

public static Map<Integer, Set<Thing>> backPackMultiple(List<Thing> things, Integer totalCost) {List<Thing> newThings = new ArrayList<>();for (Thing thing : things) {newThings.addAll(splitThing(thing));}Map<Integer, Set<Thing>> tmp = backPack01(newThings, totalCost);Map<Integer, Set<Thing>> ans = new HashMap<>();Set<Thing> oldThings = new HashSet<>(things);for (Map.Entry<Integer, Set<Thing>> x : tmp.entrySet()) {ans.put(x.getKey(),mergeThing(x.getValue(),oldThings));}return ans;
}

现在让我们来测试一下:

public static void main(String[] args) {String[][] a = {{"01-1", "3", "2", "3"}, {"01-2", "10", "4", "7"},{"01-3", "20", "8", "15"}, {"01-4", "5", "16", "31"}, {"01-5", "3", "25", "51"}};List<Thing> things = new ArrayList<>();for (String[] b : a) {Thing thing = new Thing();thing.setName(b[0]);thing.setNumber(Integer.parseInt(b[1]));thing.setWeight(Integer.parseInt(b[2]));thing.setIncome(Integer.parseInt(b[3]));things.add(thing);}Map<Integer,Set<Thing>> solveMethod = backPackMultiple(things,300);for (int i = 180; i <= 211; i++) {if (!print2(solveMethod.get(i), i)) {}}
}

与上面的测试一样,这里定义了 01-1 有3个,01-2有10个,01-3有20个,01-4有5个,01-5有3个。
我们来看看结果。

与之前错误的算法比较,这里主要有两种区别:

  1. 200之后的结果正确了
  2. 成本为199的结果收益比之前的多,之前是386,现在是388。

应用场景

每一种算法的存在都不会仅仅出现在竞赛场上,举一个简单的例子,现在你需要配一台电脑,你找到同种兼容的硬盘,现在你需要配总成本为5000的硬盘,最终存储空间越大越好,你该如何配置。你会发现,这就是一个完全背包的问题。
多重背包的场景就更多了。
这也是为什么动态规划这种算法里面会有规划两个字儿,说简单点就是这类算法都有助于我们做规划。

动态规划之完全背包和多重背包相关推荐

  1. 动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包,混合背包

    动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包 01背包 && 完全背包 && 分组背包 の 视频教程:https://www.bilibi ...

  2. 背包问题教程-01背包,完全背包,多重背包,混合背包 收藏

    P01: 01背包问题 题目 有N件物品和一个容量为V的背包.第i件物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使价值总和最大. 基本思路 这是最基础的背包问题,特点是:每种物品仅有 ...

  3. 01背包输出路径、完全背包、多重背包

    背包问题 一.01 Knapsack(输出路径- >选的物品) 二.完全背包 1.三重循环,极可能TLE,滚动数组优化后j逆向枚举 2.二重,优化消去变量k(没有特别厘清,但可以直接从完全背包角 ...

  4. hdu 3732(01背包转多重背包)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3732 思路:这么大的数据,用01背包肯定会TLE的,01背包转多重背包..最多也就11*11=121件 ...

  5. 动态规划 4、基础背包问题总结(多重背包与多重背包的转化)

    描述: 有N种物品和一个容量为V的背包.第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大. 变式:有的物品 ...

  6. 动态规划-----------01背包,完全背包与多重背包

    P01: 01背包问题 题目 有N件物品和一个容量为V的背包.第i件物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使价值总和最大. 基本思路 这是最基础的背包问题,特点是:每种物品仅有 ...

  7. 最少硬币找零系列问题(01背包,完全背包,多重背包动态规划)

    背包问题思路解决最小硬币找零系列问题. 一.01硬币找零问题(01背包) 给定不同面额的硬币 coins 和总金额 m.每个硬币最多选择一次.计算可以凑成总金额所需的最少的硬币个数.如果没有任何一种硬 ...

  8. 01背包,完全背包,多重背包,分组背包的使用条件以及代码模板

    背包问题算是动态规划中的入门题目了,背包问题有很多种.背包九讲中讲的很清楚,我就不班门弄斧了,针对几种比较常见的背包问题,阐述一下它的使用前提和代码模板. 1.01背包问题 题目 有N 件物品和一个容 ...

  9. 01背包、完全背包、多重背包

    参考(都有些错误):https://github.com/guanjunjian/Interview-Summary/blob/master/notes/algorithms/%E7%BB%8F%E5 ...

  10. 01背包, 完全背包,多重背包

    优秀博文01背包https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html 背包问题泛指以下这一种问题: 给定一组有固定价值和固定重量的 ...

最新文章

  1. 数据库 DB database SQL DBMS
  2. 求解带时间窗车辆路径问题的多目标模因算法
  3. 签约中国搜索,第四范式助力智慧媒体转型发展
  4. 信息发布webpart——网页编辑器应用攻略
  5. JavaWeb关于工程运行的笔记
  6. hive中导入csv,本地CSV导入hive表
  7. 图论算法——加权无向图的数据结构
  8. C++ 类型转换归纳
  9. hadoop api 复制文件_Hadoop核心架构是怎样的?
  10. win7虚拟机_虚拟机VMware 14安装步骤
  11. 智能合约语言 Solidity 教程系列8 - Solidity API 1
  12. python获取网页数据判断并提交_python3爬虫无法通过网页内容判断存在与否?
  13. XTU 1339 Interprime
  14. 别踩白块儿 java源码下载_“别踩白块儿游戏源代码分析和下载
  15. idea spring boot 修改html等不重启即时生效
  16. 数据分析之数据质量分析和数据特征分析
  17. 全球及中国直播平台市场发展分析及投资战略规划报告2023-2030年
  18. 转换TIFF图像为JPEG2000格式
  19. MultiPath: Multiple Probabilistic Anchor Trajectory Hypotheses for Behavior Prediction
  20. 语音信号短时域分析之短时平均能量(四)

热门文章

  1. 测试必备知识:Web 测试F12的用处
  2. Flex和Flash开发人员的Adobe Flash Player( Windows )调试器( 也称为调试播放器或内容调试器 )和独立播放器( 又名投影仪 )
  3. WSO2 ESB 5.0.0 配置 MySQL 数据源
  4. JAVA 多线程并发
  5. 利用SMB协议共享电脑文件,发挥ipad生产力
  6. 数据结构:图结构的实现
  7. Python PyQt5l表单应用 - 自定义选择下拉框样式
  8. Python-开根号的几种方式
  9. Linux获取电信超级密码,电信光猫-华为HG8245C获取超级管理员密码
  10. php调用itchat,itchat接口使用示例