1,应用场景:背包问题
- 问题描述:有一个容量为4磅的背包,需要装入如列表下的物品,在装入物品可重复和不可重复两种场景下,怎样才能使装入机制最大化
| 商品名称 | 商品重量 | 商品价格 |
|---|
| 吉他 | 1 | 1500 |
| 音响 | 4 | 3000 |
| 电脑 | 3 | 2000 |
2,动态规划算法描述
- 动态规划算法(Dynamic Programming)核心的思想是:将大问题划分为小问题进行解决,从而 一步步 获得最优解的处理算法(这个一步步是重点,等会就发现真是一步步)
- 动态规范算法和分治算法类似,基本思想都是将待解决问题分解成若干个子问题,先求解子问题,然后从子问题中得到原问题的解
- 与分治算法不同的是,适用于动态规划求解的问题,经分解得到的子问题往往不是相互独立的,下一个子阶段的求解是建立在上一个解决的求解基础上的,依次递进获取最终解
- 动态规划可以通过填表的方式来逐步推进,最终得到最优解
3,动态规划算法的最佳实践——背包问题
- 背包问题,就是给定一个容量的背包,依次放入物品,装入物品可从可重复和不可重复两个维度分析;装入物品总重量不能超过背包容量
- 在这个过程中,可以通过一个二维表格来分析:
- 横行表示背包容量从0到指定容量的各种情况,这是第一步的分,将大容量的背包先转化为小容量背包,算出子问题的最优解,然后一步步加大容量,算出最终问题的最优解
- 纵行表示商品信息,且第一横行为空值,作为初始数据的对比值;纵行是第二步的分,先将一个商品放入背包中,算出最优解,逐渐增加商品类型和商品数量,算出最终最优解
- 最终表格的最右下角的格子,即为数据的最优解
3.1,不可重复表格填充演示
| 物品/背包容量 | 0磅 | 1磅 | 2磅 | 3磅 | 4磅 |
|---|
| 第一横行0 | 0 | 0 | 0 | 0 | 0 |
| 吉他|1|1500 | 0 | 1500 | 1500 | 1500 | 1500 |
| 音响|4|3000 | 0 | 1500 | 1500 | 1500 | 3000 |
| 电脑|3|2000 | 0 | 1500 | 1500 | 2000 | 3500 |
- 第一横行和第一纵行统一初始化为0,作为后续数据的初始化对比
- 开始填入物品,从第二横行开始,此时只有一个物品能填入背包,在填入时用物品重量和背包容量进行比较,能填充进去,直接填充并修改背包价值,不能填充进入则背包容量依旧为0,因为不可重复,不考虑放两个一样的,则对于第二横行来说, 吉他重量为1,在背包无论容量是多少,最大价值都是1500
- 继续填充第三横行,此时可装入背包的物品变为两个,则开始存在判断
- 如果当前物品重量大于背包容量,则不能装入,背包价值沿用该列上一行的价值,即
table[i-1][j],适用于第三横行的前三磅; - 如果当前物品重量大于等于背包容量,此处为一种情况,分析中可以分两部分分析,先进行等于分析,装入第四行时进行大于分析
- 如果当前商品重量等于背包容量,则背包可以装入该商品,但是需要对场景进行分析,此时背包已经装入了其他商品,把其他商品清空,装入该商品后,价值不一定大于原价值,所以需要进行比较,取
Max(customerPrvice + 0, table[i-1][j]),即用当前商品价值与该列上一行的价值进行比较,取最大值为当前背包价值,此处商品加0是因为相等,大于第四行分析;所以在第三种第四列中,4磅背包容量时,可以装入音响,商品价值为3000 > 原来的1500,装入3000
- 继续填充第四横行,因为商品重量为3磅,所以0-2磅不符合,直接取上一行的值;3磅时,电脑价值3000 > 原来价值1500,填充为电脑价值,重要的是第四列,背包容量大于当前商品容量时:
- 背包容量大于当前商品容量,首先必须装入当前商品,依次来获取一个比对价值与上一行价值进行比较来获取一个最优解,但是在装入当前商品时,背包还有容量,此时
4 - 3 = 1磅,背包还可以装入1磅的商品,则需要从上一行背包容量为1磅的位置,取出背包此时的最高价值与当前商品价值相加,形成在该位置处的当前商品所能带来的最高价值,然后在于该列上一行进行比较,即Max(customerPrice + table[i-1][packageCapacity - customerWeight], table[i-1][j]);此时放入本商品的价值是2000 + 1500 = 3500,本列上一行的价值是3000,则直接覆盖 - customerPrice:当前商品价格,customerWeight:当前商品重量,packageCapacity:背包容量,
packageCapacity - customerWeight:背包剩余重量,table[i-1][packageCapacity - customerWeight]:背包在当前商品类型剩余重量下的最优解 - 此处必须从上一行找剩余容量的最优解,原因在于商品不能重复,如果从本行找最优解,可能剩余容量的最优解中已经包含了当前商品
3.2,可重复表格填充演示
| 物品/背包容量 | 0磅 | 1磅 | 2磅 | 3磅 | 4磅 |
|---|
| 第一横行0 | 0 | 0 | 0 | 0 | 0 |
| 吉他|1|1500 | 0 | 1500 | 3000 | 4500 | 6000 |
| 音响|4|3000 | 0 | 1500 | 3000 | 4500 | 6000 |
| 电脑|3|2000 | 0 | 1500 | 3000 | 4500 | 6000 |
- 可重复放入与不可重复放入逻辑基本一致,不过有一个点需要注意,就是在背包容量大于商品重量是,需要在当前商品行找剩余重量的最优解,而不是上一行,因为商品可以重复!
- 依次逻辑填充第二横行,因为吉他重量是1磅,在1磅时,价值为1500;2磅是,剩余容量为1磅,则取1磅的最优解1500,价值为3000;3磅是,剩余价值为2磅,取2磅最优解3000,价值为4500;4磅为6000
- 继续填充第三横行,前三磅分别取上一行最优解;4磅处,用3000与6000取最大值6000
- 继续填充第四横行,前两磅分别取上一行最优解;3磅处,用2000与3000取最大值3000;4磅处,用
2000 + 1500 = 3500与6000取最大值6000
- 动态规划背包问题算法解析基本完成,可以看到,就是将问题尽量划小,随着背包容量不断增加,商品种类不断增多,来最终获取到预期的最优解
4,代码实现
package com.self.datastructure.algorithm.dynamic;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
public class KnapsackProblem {
public static void main(String[] args) {
String[] nameArr = {"吉他", "音响", "电脑"};
int[] weightArr = {1, 4, 3};
int[] priceArr = {1000, 3000, 6000};
int packageCapacity = 6;
backpackWithoutRepeat(nameArr, weightArr, priceArr, packageCapacity);
backpackWithRepeat(nameArr, weightArr, priceArr, packageCapacity);
}
private static void backpackWithRepeat(String[] nameArr, int[] weightArr, int[] priceArr, int packageCapacity) {
int[][] packageArr = new int[nameArr.length + 1][packageCapacity + 1];
String[][] contentArr = new String[nameArr.length + 1][packageCapacity + 1];
for (int i = 1; i < packageArr.length; i++) {
for (int j = 1; j < packageArr[i].length; j++) {
if (weightArr[i - 1] > j) {
packageArr[i][j] = packageArr[i - 1][j];
} else {
int onePart = packageArr[i - 1][j];
int otherPart = priceArr[i - 1] + packageArr[i][j - weightArr[i - 1]];
packageArr[i][j] = Math.max(onePart, otherPart);
if (packageArr[i][j] == onePart) {
contentArr[i][j] = contentArr[i - 1][j];
} else {
contentArr[i][j] = nameArr[i - 1] + "," +
(StringUtils.isEmpty(contentArr[i][j - weightArr[i - 1]])
? ""
: contentArr[i][j - weightArr[i - 1]]);
}
}
}
}
System.out.println("背包价值: " + packageArr[nameArr.length][packageCapacity]);
System.out.println("背包内容: " + contentArr[nameArr.length][packageCapacity]);
}
private static void backpackWithoutRepeat(String[] nameArr, int[] weightArr, int[] priceArr, int packageCapacity) {
int[][] packageArr = new int[nameArr.length + 1][packageCapacity + 1];
int[][] contentArr = new int[nameArr.length + 1][packageCapacity + 1];
for (int i = 1; i < packageArr.length; i++) {
for (int j = 1; j < packageArr[i].length; j++) {
if (weightArr[i - 1] > j) {
packageArr[i][j] = packageArr[i - 1][j];
} else {
int onePart = packageArr[i - 1][j];
int otherPart = priceArr[i - 1] + packageArr[i - 1][j - weightArr[i - 1]];
packageArr[i][j] = Math.max(onePart, otherPart);
if (otherPart == packageArr[i][j]) {
contentArr[i][j] = 1;
}
}
}
}
int totalPrice = 0;
int maxLine = contentArr.length - 1;
int maxColumn = contentArr[0].length - 1;
for (;maxLine > 0 && maxColumn > 0;) {
if (contentArr[maxLine][maxColumn] == 1) {
maxColumn -= weightArr[maxLine - 1];
totalPrice += priceArr[maxLine - 1];
System.out.printf("%s 加入了背包 \n", nameArr[maxLine - 1]);
}
maxLine--;
}
System.out.println("不重复情况下, 背包可容纳的最大价值: " + totalPrice);
}
}