题目:
菜地里有四件蔬菜: 土豆[重量:2, 价值:300] 红薯[重量:1, 价值:150] 萝卜[重量:3, 价值:200] 青菜[重量:1, 价值:200] 兔子有个容量为4(最大重量4)的背包,怎么装能够实现背包价值最大?
建模
物品对象:
/**
* 物品对象
* 所有物品需要继承此类
*/
class Item {
// 物品名称
private String name;
// 物品重量
private int itemWeight;
// 物品价值
private int itemValue;
public int getItemWeight() {
return itemWeight;
}
public void setItemWeight(int itemWeight) {
this.itemWeight = itemWeight;
}
public int getItemValue() {
return itemValue;
}
public void setItemValue(int itemValue) {
this.itemValue = itemValue;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Item(int itemWeight, int itemValue) {
this.itemWeight = itemWeight;
this.itemValue = itemValue;
}
public Item(String name, int itemWeight, int itemValue) {
this.name = name;
this.itemWeight = itemWeight;
this.itemValue = itemValue;
}
}
背包对象:
/**
* 背包建模
* @author zx
* @date 2021年12月20日
*/
public class Package {
/**
* 背包容量
*/
private int maxWeight = 0;
/**
* 背包内物品的当前价值
*/
private int currentValue = 0;
/**
* 背包内物品的当前重量
*/
private int currentWeight = 0;
/**
* 背包内所装物品的集合
*/
private List<Item> items = new ArrayList<>();
public Package addItem(Item item){
if(collectionIsEmpty(items)){
Package oneItemPackage = new Package();
oneItemPackage.setMaxWeight(item.getItemWeight());
oneItemPackage.setCurrentValue(item.getItemValue());
oneItemPackage.setCurrentWeight(item.getItemWeight());
List<Item> itemShadow = new ArrayList<>();
itemShadow.add(item);
oneItemPackage.setItems(itemShadow);
return oneItemPackage;
}else{
Package manyItemPackage = new Package();
manyItemPackage.setMaxWeight(this.maxWeight += item.getItemWeight());
manyItemPackage.setCurrentValue(this.currentValue += item.getItemValue());
manyItemPackage.setCurrentWeight(this.currentWeight += item.getItemWeight());
List<Item> itemShadow = new ArrayList<>(items);
itemShadow.add(item);
manyItemPackage.setItems(itemShadow);
return manyItemPackage;
}
}
public Package() {}
public Package(int weight, int currentValue) {
this.maxWeight = weight;
this.currentValue = currentValue;
}
public int getMaxWeight() {
return maxWeight;
}
public void setMaxWeight(int maxWeight) {
this.maxWeight = maxWeight;
}
public int getCurrentWeight() {
return currentWeight;
}
public void setCurrentWeight(int currentWeight) {
this.currentWeight = currentWeight;
}
public int getCurrentValue() {
return currentValue;
}
public void setCurrentValue(int currentValue) {
this.currentValue = currentValue;
}
public List<Item> getItems() {
return items;
}
public void setItems(List<Item> items) {
this.items = items;
}
public boolean collectionIsEmpty(@SuppressWarnings("rawtypes") Collection collection){
return collection == null || collection.size() == 0;
}
}
算法分析
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* 动态规划 - 基本背包
* @author zx
* @date 2021年12月30日
*/
public class BagUtils {
/**
* 问题:菜地里有四棵蔬菜: {土豆[重量:2, 价值:300],红薯[重量:1, 价值:150],萝卜[重量:3, 价值:200],青菜[重量:1, 价值:200]}
* 兔子有个容量为4(最大重量4)的背包,怎么装能够实现背包价值最大?
*
* 思路
* x容量\y物品 包容量:1 包容量:2 包容量:3 包容量:4
* 土豆 装不下,0 300 300 300
* 红薯 150 300 450 ?
* 萝卜
* 青菜
* 对于每一个格子下的当前物品,只存在装与不装的情况。能装下则背包价值更新,装不下则保持上一列的总价值
* 不装新物品: MaxValue = 当前容量上一行当前容量的价值。 (红薯上一行是土豆。在包容量:4的列,不装红薯,当前包内物品最大价值为土豆 = 300)
* 装新物品【能装下】: MaxValue = 当前物品的价值 + (背包总容量 - 当前物品的容量)能装的最大价值。
*
* (在包容量:4的列,当前物品的价值 = 红薯的价值 150-重量:1 + 剩余背包容量3时,能装下的除红薯外最大价值的物品。
* 剩余背包容量3时,能装下的除红薯外最大价值的物品等于什么呢?
* 这个在之前已经计算过了,在仅有土豆红薯情况下,上表土豆行在容量3时最大能装价值300的物品。所以在包容量:4的列,当前物品的价值 = 红薯的价值 150+土豆行在容量3时最大能装价值300 = 450.
* 总结:状态转移公式:
* Max[i,packageWeight] = MAX( 不装当前物品:Max[i-1,packageWeight]上一行本列背包所装物品的最大价值。 VS 装当前物品:Max[i-1, packageWeight - 当前item重量]。 )
*/
public static void main(String[] args) {
// 物品列表
List<Item> items = new ArrayList<>();
Item item1 = new Item("土豆",2, 300);
Item item2 = new Item("红薯",1, 150);
Item item3 = new Item("萝卜",3, 200);
Item item4 = new Item("青菜",1, 200);
items.add(item1);
items.add(item2);
items.add(item3);
items.add(item4);
/*
* List<List<Package>> calMaxValueCache 用于缓存计算过的节点数据。
* index0 存储第一行【第一个物品分别在背包容量1、2、3、4....n】的最大价值 计算数据
* eg: 背包容量0 背包容量1 背包容量2 背包容量3 背包容量4
* 土豆[重量2,价值300] 0 0 300 300 300
* 红薯[重量1,价值150] 0 150 300 450 450
* 则:
* calMaxValueCache index0 中存储的数据为 :[0, 0, 300, 300, 300]
* calMaxValueCache index1 中存储的数据为 :[0, 150, 300, 450, 450]
*/
List<List<Package>> calMaxValueCache = new ArrayList<>();
//双层循环代表:轮询每个物品在 背包容量 1、2、3、4...n 下的所有情况,itemIndex对应行
for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
List<Package> x1yn = new ArrayList<>();
// packageWeight 对应列
for (int packageWeight = 0; packageWeight <= 4; packageWeight++) {
//第n个物品,在背包容量为 0、1、2、3、4 下,calMaxValueCache index0 中存储的数据为 :[0, 0, 300, 300, 300]
Item item = items.get(itemIndex);
int itemValue = item.getItemValue();
int itemWeight = item.getItemWeight();
// 第一个物品第一行,对应背包容量1234,不需要特殊计算,只需要看能否装下当前物品即可
if(itemIndex == 0){
Package oneItemPackage = new Package();
if(itemWeight <= packageWeight){
// 装的下才会更新背包最大价值
oneItemPackage = oneItemPackage.addItem(item);
}
// 存储计算过的结果数据
x1yn.add(oneItemPackage);
continue;
}
// 第二行开始遵循以下规律
// Max[i,packageWeight] = MAX( 不装当前物品:Max[i-1,packageWeight], 装当前物品:Max[i-1, packageWeight - 当前item重量] )
/*
* 上一整行【上个物品】计算好的价值信息
* calMaxValueCache index0 中存储的数据为 :[0, 0, 300, 300, 300]
* calMaxValueCache index1 中存储的数据为 :[0, 150, 300, 450, 450]
*/
List<Package> xny_1Cache = calMaxValueCache.get(itemIndex -1);
// 【不装本物品】,当前最大背包物品价值 = 上一行本列对应背包所装的物品价值 = 不装当前物品:Max[i-1,packageWeight]
Package unAddThisItemPackage = new Package();
if(packageWeight != 0){
unAddThisItemPackage = xny_1Cache.get(packageWeight);
}
int unAddThisItemPackageValue = unAddThisItemPackage.getCurrentValue();
// 【装当前物品】,不考虑装不下,装不下会取【不装本物品】的情况
Package addThisItemPackage = new Package();
if(itemWeight <= packageWeight){
// 将当前物品放入背包,此时背包会装满或剩余空间
addThisItemPackage = addThisItemPackage.addItem(item);
// 计算剩余空间还能装下的最大的重量的物品(除本物品外),在之前已经计算过了,所以此处直接查找对应背包容量列,最大价值的格子。
int leftRoom = packageWeight - itemWeight;
if(leftRoom > 0){
// 查询剩余空间在之前计算过的,leftRoom大小空间能够装的最大的物品
List<Package> leftRoomCol = new ArrayList<>();
for (int itemIndexExceptSelf = 1; itemIndexExceptSelf <= calMaxValueCache.size(); itemIndexExceptSelf++) {
// n-1列(当前物品只有一件,不能将自己多次装入)
List<Package> packages = calMaxValueCache.get(itemIndexExceptSelf - 1);
// 背包剩余空间leftRoom 代表的是背包容量所在的列「下标」
Package aPackage1 = packages.get(leftRoom);
leftRoomCol.add(aPackage1);
}
if(leftRoomCol.size() > 0){
// calMaxValueCache 中上一列中,除本物品外,价值最高的物品所在的背包
Package maxValuePackage = leftRoomCol.stream().max(
Comparator.comparingInt(Package::getCurrentValue)).get();
// 将背包物品装进 【装当前物品】 情况中剩余的背包空间中
List<Item> maxValuePackageItems = maxValuePackage.getItems();
for (Item maxValuePackageItem : maxValuePackageItems) {
addThisItemPackage = addThisItemPackage.addItem(maxValuePackageItem);
}
}
}
}
// 不装当前物品:Max[i-1,packageWeight] VS 装当前物品:Max[i-1, packageWeight - 当前item重量] ,取价值更高的情况
int addThisItemPackageValue = addThisItemPackage.getCurrentValue();
if(addThisItemPackageValue >= unAddThisItemPackageValue){
x1yn.add(addThisItemPackage);
}else {
x1yn.add(unAddThisItemPackage);
}
}
// 缓存本列计算完成的值
calMaxValueCache.add(x1yn);
}
// 表格形式打印所有情况
for (List<Package> packages : calMaxValueCache) {
for (Package aPackage : packages) {
System.out.print(aPackage.getCurrentValue()+"\t");
}
System.out.println();
}
}
}
结果:
背包容量 0 1 2 3 4
土豆 0 0 300 300 300
红薯 0 150 300 450 450
萝卜 0 150 300 450 450
青菜 0 200 350 500 650