李大爷的动态规划学习记录

李大爷的动态规划学习记录

前言

又是一个没学会觉得很牛逼,其实学之前就用过的思想方式。
本文章是学习记录,所以都是案例,可能需要看着参考阅读更佳。

一、斐波那契

1.1:规则

每一级的结果都是前两级结果之和

例如:[0,1,1,2,3,5,7,12,19,31,50,71…]

1.2:子问题

  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:规则

  1. 数组中不相邻的多个金额相加,得出最多可得金额
  2. 得出最多金额方案所需的房号

例如:[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]
九宫格:

238
475
297

结果:2+4+7+9+7=29

3.2:子问题

是左边分数高,还是上边分数高
推导过程:

2513
61318
82229

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,有以下物件:

物品重量价值
16540
23200
34180
45350
5160
62150
73280
85450
94320
102120

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;
    }
}

六、没有了

先这样吧,这篇文章目的是了解动态规划是个什么玩意,再深入的看看参考的视频,视频不是我的,不过觉得说得挺好的。

总结

  1. 先找到规律,拆解子任务,先由【递归】实现
  2. 【动态规划】是递归逻辑的优化方案,通过空间换时间的方案
  3. 【动态规划】是带“备忘录”的“从下往上”拆解的思想
  4. 【状态压缩】是对动态规划的【空间复杂度】的优化
  5. 【动态规划】的两个必要条件,【最优子结构】【无后效性】
    5.1 【最优子结构】:可以被拆分成规律一样的子问题(可以递归)
    5.2 【无后效性】:父问题不影响子问题的结果,父问题不需要知道子问题的推导过程

参考

B站视频:动态规划入门系列课程全集

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值