李大爷的动态规划学习记录
前言
又是一个没学会觉得很牛逼,其实学之前就用过的思想方式。
本文章是学习记录,所以都是案例,可能需要看着参考阅读更佳。
一、斐波那契
1.1:规则
每一级的结果都是前两级结果之和
例如:[0,1,1,2,3,5,7,12,19,31,50,71…]
1.2:子问题
- 【上一级】数值是多少
- 【上上级】数值是多少
1.3:代码
public class DpTest {
public static void main(String[] args) {
long time1 = System.currentTimeMillis();
// 递归
System.out.println(fblq(40));
long time2 = System.currentTimeMillis();
System.out.println("耗时:"+(time2-time1));
// 缓存递归
System.out.println(fblqMap(40, new HashMap<>(40)));
long time3 = System.currentTimeMillis();
System.out.println("耗时:"+(time3-time2));
// 倒叙
System.out.println(fblqDesc(40));
long time4 = System.currentTimeMillis();
System.out.println("耗时:"+(time4-time3));
}
/**
* 递归实现斐波那契
* @param num
* @return
*/
private static long fblq(int num) {
// 0和1没必要算,直接返回
if (num == 0) { return 0L; }
if (num == 1) { return 1L; }
// 结果:num-1的结果 + num-2的结果
return fblq(num-1)+fblq(num-2);
}
/**
* 带缓存递归斐波那契
* @param num
* @param map
* @return
*/
private static long fblqMap(int num, Map<Integer, Long> map) {
// 查缓存是否已有结果
Long res = map.get(num);
if (!Objects.isNull(res)) { return res; }
// 如果没有结果再换算
if (num == 0) { res = 0L; }
if (num == 1) { res = 1L; }
if (Objects.isNull(res)) {
res = fblqMap(num-1,map)+fblqMap(num-2,map);
}
// 缓存下结果
map.put(num,res);
return res;
}
/**
* 从下往上的斐波那契
* @param num
* @return
*/
private static long fblqDesc(int num) {
// 创建一个参照数组,因为包含0,所以长度需要多一位
long[] arr = new long[++num];
for (int i = 0; i < num; i++) {
// 0和1没有参照值,直接赋予固定值
if (i < 2) {
arr[i] = i;
continue;
}
// 参照前两个值,算出当前值
arr[i] = arr[i-1]+arr[i-2];
}
return arr[num-1];
}
}
二、打家劫舍(含状态压缩)
2.1:规则
- 数组中不相邻的多个金额相加,得出最多可得金额
- 得出最多金额方案所需的房号
例如:[8,7,4,5]
最高金额:13
所需房号:[0,3]
2.2:子问题
是劫【这家】和【下下家】,还是劫【下家】
推导过程:[8,8,12,13]
2.3:代码
public class DpTest {
public static void main(String[] args) {
int[] homes = new int[]{15,8,2,7,1,11,15,8,2,7,1,11,15,8,2,7,1,11,15,8,2,7,1,11,15,8,2,7,1,11,15,8,2,7,1,11,15,8,2,7,1,0};
long time1 = System.currentTimeMillis();
System.out.println(djjs(homes, homes.length - 1));
long time2 = System.currentTimeMillis();
System.out.println("耗时:"+(time2-time1));
System.out.println(djjsDesc(homes));
long time3 = System.currentTimeMillis();
System.out.println("耗时:"+(time3-time2));
System.out.println(djjsDescCompression(homes));
long time4 = System.currentTimeMillis();
System.out.println("耗时:"+(time4-time3));
List<Integer> resHomes = djjsDescNums(homes);
System.out.println("被劫房号:"+ resHomes);
int checkHomes = 0;
for (Integer resHome : resHomes) {
checkHomes += homes[resHome];
}
System.out.println("检验被劫房号准确性:"+ checkHomes);
}
/**
* 递归实现打家劫舍
* @param homes
* @param index
* @return
*/
private static int djjs(int[] homes, int index) {
// 避免越界
if (index < 0) { return 0; }
// 比大小【下一位】【下下位+当前位】
return Math.max(djjs(homes,index-1),djjs(homes,index-2)+homes[index]);
}
/**
* 从下往上的打家劫舍
* @param homes
* @return
*/
private static int djjsDesc(int[] homes) {
int len = homes.length;
// 创建一个长度一样的参照数组
int[] res = new int[len];
// 前两个值不用参考,直接给出结果
// 只有他一个,最大值就他自己
res[0] = homes[0];
// 有两个,取最大值
res[1] = Math.max(homes[0],homes[1]);
for (int i = 2; i < len; i++) {
// 比大小【下一位(参照)】【下下位(参照)+当前位】
res[i] = Math.max(res[i-1],res[i-2]+homes[i]);
}
// 末尾的结果就是最终结果
return res[len-1];
}
/**
* 从下往上的打家劫舍(状态压缩版)
* @param homes
* @return
*/
private static int djjsDescCompression(int[] homes) {
int len = homes.length;
// 创建两个参照变量,代替参照数组
// 前两个值不用参考,直接给出结果
int r1 = homes[0];
int r2 = Math.max(homes[0],homes[1]);
for (int i = 2; i < len; i++) {
// 比大小【下一位(参照)】【下下位(参照)+当前位】
int r3 = Math.max(r2,r1+homes[i]);
// 轮换参照变量值
r1 = r2;
r2 = r3;
}
// 最终结果是r3,r3赋值给了r2
return r2;
}
/**
* 打家劫舍(取房号版)
* @param homes
* @return
*/
private static List<Integer> djjsDescNums(int[] homes) {
int len = homes.length;
// 创建一个长度一样的参照数组
int[] res = new int[len];
// 前两个值不用参考,直接给出结果
// 只有他一个,最大值就他自己
res[0] = homes[0];
// 有两个,取最大值
res[1] = Math.max(homes[0],homes[1]);
for (int i = 2; i < len; i++) {
// 比大小【下一位(参照)】【下下位(参照)+当前位】
res[i] = Math.max(res[i-1],res[i-2]+homes[i]);
}
// 房号结果
List<Integer> homeNums = new ArrayList<>();
// 最大金额
int max = res[res.length-1];
for (int i = res.length-1; i > 0; i--) {
// 下个房间比当前房间累计金额少,说明当前房间被劫了
if (res[i-1] < max) {
// 记下房号
homeNums.add(i);
// 当前累计金额 - 当前房间金额 = 下一个劫取后的累计金额
max = res[i] - homes[i];
}
}
// 最后再查下第1个房间
if (max == res[0]) { homeNums.add(0); }
return homeNums;
}
}
三、礼物的最大价值
3.1:规则
在九宫格中,从左上往右下移动,每次只能“向右”或“向下”,算出可总分数的最高值
比如:[2,3,8,4,7,5,2,9,7]
九宫格:
2 | 3 | 8 |
4 | 7 | 5 |
2 | 9 | 7 |
结果:2+4+7+9+7=29
3.2:子问题
是左边分数高,还是上边分数高
推导过程:
2 | 5 | 13 |
6 | 13 | 18 |
8 | 22 | 29 |
3.3:代码
public class DpTest {
public static void main(String[] args) {
int[] arr = new int[]{2,3,8,4,7,5,2,9,7};
System.out.println(zdjz(arr, arr.length - 1));
System.out.println(zbjzDesc(arr));
}
/**
* 递归算出礼物的最大价值
* @param arr
* @return
*/
private static int zdjz(int[] arr, int index) {
// 到了起点
if (index == 0) { return arr[0]; }
// 只能选左边
if (index < 3) { return zdjz(arr,index-1)+arr[index]; }
// 只能选上边
if (index == 3 || index == 6) { return zdjz(arr,index-3)+arr[index]; }
// 左边或上边,选择最大值
return Math.max(zdjz(arr,index-1),zdjz(arr,index-3))+arr[index];
}
/**
* 从下往上算出礼物的最大价值
* @param arr
* @return
*/
private static int zbjzDesc(int[] arr) {
int len = arr.length;
int[] res = new int[len];
res[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
// 只能选左边
if (i < 3) {
res[i] = res[i-1]+arr[i];
continue;
}
// 只能选上边
if (i == 3 || i == 6) {
res[i] = res[i-3]+arr[i];
continue;
}
// 左边或上边,选择最大值
res[i] = Math.max(res[i-1],res[i-3])+arr[i];
}
System.out.println(Arrays.toString(res));
return res[len-1];
}
}
四、零钱兑换
4.1:规则
在有限面额的零钱基础上,凑够规定金额,且所需的零钱数量最少。
如果无法凑出,返回-1。
例如:
- 面额:[3,7,11]
- 金额:50
结果:6(11+11+11+11+3+3=50)
4.2:子问题
这题【递归】和【动态规划】的解题理解有差异,递归是根据“面额”向下递减,动态规划是根据“金额”1元1元向上递增
递归:“金额”扣除各“面额”后的最少拼凑次数
动态规划:“金额”每一元的最少拼凑次数
推导过程:
[0, -1, -1, 1, -1, -1, 2, 1, -1, 3, 2, 1, 4, 3, 2, 5, 4, 3, 2, 5, 4, 3, 2, 5, 4, 3, 6, 5, 4, 3, 6, 5, 4, 3, 6, 5, 4, 7, 6, 5, 4, 7, 6, 5, 4, 7, 6, 5, 8, 7, 6]
4.3:代码
public class DpTest {
public static void main(String[] args) {
int[] moneys = new int[]{3,7,11};
System.out.println(lqdh(moneys, 50));
System.out.println(lqdhDesc(moneys, 50));
System.out.println(lqdhDescMoneys(moneys, 50));
}
/**
* 递归实现零钱兑换
* @param moneys
* @param amt
* @return
*/
private static int lqdh(int[] moneys, int amt) {
// 如果金额为0,不用计算
if (amt == 0) { return 0; }
// 累计比较最小值
int minRes = Integer.MAX_VALUE;
// 判断本次推导是否生效
boolean success = false;
for (int i = 0; i < moneys.length; i++) {
int money = moneys[i];
// 校验当前面额是否有效
if (money > amt) { continue; }
int lqdhRes = lqdh(moneys, amt - money);
// 校验下层推导是否生效
if (lqdhRes == -1) { continue; }
minRes = Math.min(minRes, lqdhRes+1);
// 本次推导生效
success = true;
}
return success?minRes:-1;
}
/**
* 从下往上实现零钱兑换
* @param moneys
* @param amt
* @return
*/
private static int lqdhDesc(int[] moneys, int amt) {
int[] resArr = new int[amt+1];
// 0元没必要算
resArr[0] = 0;
// 1块钱1块钱递增上去算最优解
for (int i = 1; i <= amt; i++) {
// 累计比较最小值
int minRes = Integer.MAX_VALUE;
// 判断本次推导是否生效
boolean success = false;
for (int money : moneys) {
// 校验当前面额是否有效
if (money > i) { continue; }
int lqdhRes = resArr[i - money];
// 校验下层推导是否生效
if (lqdhRes == -1) { continue; }
minRes = Math.min(minRes, lqdhRes+1);
// 本次推导生效
success = true;
}
resArr[i] = success?minRes:-1;
}
return resArr[resArr.length-1];
}
/**
* 从下往上实现零钱兑换(取兑换面额版)
* @param moneys
* @param amt
* @return
*/
private static List<Integer> lqdhDescMoneys(int[] moneys, int amt) {
// 由于java数组类型容量是创建时固定的,无法实现,所以用list代替
// 模拟二位数组,如:[[3],[3,7,7],[7,11]]
List<List<Integer>> resArr = new ArrayList<>(amt+1);
// 0元没必要算
resArr.add(new ArrayList<>());
// 1块钱1块钱递增上去算最优解
for (int i = 1; i <= amt; i++) {
// 累计比较最小值(size最小)
List<Integer> minRes = new ArrayList<>();
// 判断本次推导是否生效
boolean success = false;
for (int money : moneys) {
// 校验当前面额是否有效
if (money > i) { continue; }
List<Integer> lqdhRes = resArr.get(i - money);
// 校验下层推导是否生效
if (Objects.isNull(lqdhRes)) { continue; }
// 以size最小作为结果
if (minRes.isEmpty()
|| minRes.size() > lqdhRes.size()+1) {
minRes = new ArrayList<>(lqdhRes);
minRes.add(money);
}
// 本次推导生效
success = true;
}
resArr.add(success?minRes:null);
}
return resArr.get(resArr.size()-1);
}
}
五、01背包问题
5.1:规则
在有限背包容量的条件下,装入尽可能多的有价值的物件
如重量上限30,有以下物件:
物品 | 重量 | 价值 |
---|---|---|
1 | 6 | 540 |
2 | 3 | 200 |
3 | 4 | 180 |
4 | 5 | 350 |
5 | 1 | 60 |
6 | 2 | 150 |
7 | 3 | 280 |
8 | 5 | 450 |
9 | 4 | 320 |
10 | 2 | 120 |
5.2:子问题
递归:随便一件物品放入后,剩余物品可得的最大价值是多少
动态规划:重量上限每增加1,的最大价值是多少
推导过程:[最大价值,所用物品]
[[0, 0], [60, 16], [150, 32], [280, 64], [340, 80], [450, 128], [540, 1], [600, 17], [730, 192], [820, 65], [880, 81], [990, 129], [1050, 145], [1140, 161], [1270, 193], [1330, 209], [1420, 225], [1480, 241], [1590, 449], [1650, 465], [1740, 481], [1800, 497], [1860, 993], [1940, 483], [2000, 499], [2090, 489], [2150, 505], [2210, 1001], [2290, 491], [2350, 507], [2410, 1003]]
5.3:代码
public class DpTest {
public static void main(String[] args) {
int limit = 30;
int[][] goods = new int[][]{
new int[]{6,540},
new int[]{3,200},
new int[]{4,180},
new int[]{5,350},
new int[]{1,60},
new int[]{2,150},
new int[]{3,280},
new int[]{5,450},
new int[]{4,320},
new int[]{2,120}
};
System.out.println(bb01(goods,limit));
System.out.println(bb01Desc(goods,limit));
}
/**
* 递归推导01背包问题
* @param goods
* @param limit
* @return
*/
private static int bb01(int[][] goods, int limit) {
int goodsLen = goods.length;
// 数组是空,没有物品
if (goodsLen < 1) { return 0; }
int max = 0;
for (int[] good : goods) {
// 重量
int weight = good[0];
// 价值
int value = good[1];
// 物品重量超过限制重量的上限,不放背包里
if (limit < weight) { continue; }
// 重新组合新数组,排除当前的物品
int[][] nextGoods = new int[goodsLen -1][];
int i = 0;
for (int j = 0; j < nextGoods.length; j++) {
if (goods[j] == good) { ++i; }
nextGoods[j] = goods[j+i];
}
// 减去当前物品重量后的最大价值+当前物品的价值
// 比较最大值
max = Math.max(bb01(nextGoods, limit - weight) + value, max);
}
return max;
}
/**
* 从下向上推导01背包问题
* @param goods
* @param limit
* @return
*/
private static int bb01Desc(int[][] goods, int limit) {
// 二维数组记录每一元的[最大价值,放入的商品]
int[][] maxArrBox = new int[limit+1][];
for (int i = 0; i <= limit; i++) {
// 初始化状态[无价值,无已用商品]
maxArrBox[i] = new int[]{0,0};
for (int j = 0; j < goods.length; j++) {
int[] good = goods[j];
// 重量
int weight = good[0];
// 价值
int value = good[1];
// 物品重量超过限制重量的上限,不放背包里
if (i < weight) { continue; }
// 已用物品明细
int maxIndex = maxArrBox[i - weight][1];
// 排除物品重复计算
if (isTrue(maxIndex,j)) { continue; }
// 比较最大价值,如果是最大,就更新状态
int maxValue = maxArrBox[i-weight][0]+value;
if (maxValue > maxArrBox[i][0]) {
// 更新最大价值
maxArrBox[i][0] = maxValue;
// 更新已用物品明细
maxArrBox[i][1] = indexToTrue(maxIndex,j);
}
}
}
return maxArrBox[maxArrBox.length-1][0];
}
/**
* 查看当前商品是否已放入包里
* @param num 标识数字(转换成二进制用)
* @param index 商品位置(二进制位下标)
* @return true:已放入,false:未放入
*/
private static boolean isTrue(int num, int index) {
int toIndex = num >> index;
return toIndex > (toIndex ^ 1);
}
/**
* 把标识中的商品位置改成true
* @param num 标识数字(转换成二进制用)
* @param index 商品位置(二进制位下标)
* @return 更改后的标识数字
*/
private static int indexToTrue(int num, int index) {
int trueIndex = 1 << index;
int res = trueIndex ^ num;
return res > num ? res : num;
}
}
六、没有了
先这样吧,这篇文章目的是了解动态规划是个什么玩意,再深入的看看参考的视频,视频不是我的,不过觉得说得挺好的。
总结
- 先找到规律,拆解子任务,先由【递归】实现
- 【动态规划】是递归逻辑的优化方案,通过空间换时间的方案
- 【动态规划】是带“备忘录”的“从下往上”拆解的思想
- 【状态压缩】是对动态规划的【空间复杂度】的优化
- 【动态规划】的两个必要条件,【最优子结构】【无后效性】
5.1 【最优子结构】:可以被拆分成规律一样的子问题(可以递归)
5.2 【无后效性】:父问题不影响子问题的结果,父问题不需要知道子问题的推导过程