从动态规划到贪心算法

动态规划入门贪心算法

为什么这篇文章是关于动态规划到贪心算法呢?主要是贪心算法比较需要快速的反应和大量的题型才可以一眼看出,而贪心算法背后都有一个复杂的动态规划,所以以动态规划来引入贪心算法是比较容易推出贪心算法模式的。
上文补充一些动态规划的其他题型,之后的每节都会补充几个系列的动态规划思路的经典题型来巩固这个算法。废话不多少。开整。

动态规划例题精解

这里没有记录一些简单的动态规划问题,但是有几个比较重要的动规思路,有名的背包系列,字符串序列问题,图问题等

打家劫舍 I

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

对于这个题目来说,直接找出他的状态方程如果是新手可能有点麻烦,但是找到他的子问题结构还是不难的。如果感觉有点困难可以考虑看一下上一篇文章:

人人都会动态规划

我们考虑此时的问题规模是n的,使用减而治之的思路,当我需要最后一个房屋获得最大价值时,就存在两种情况,1. 一种是最后一个房屋拿,2. 一个是不拿
所以,只需要比较两种情况谁拿的最多就好
考虑一下,第一种情况拿了,那么后面只能隔一个拿了,所以也就是拿隔一个房屋的最大价值。
第二种情况不拿,那么这个房屋就像不存在一样,直接就去拿前一个的最大价值

上述分析我们其实都是以一个函数作为基础的,这个函数的作用其实就是返回位置为i时可以获得的最大价值。

递归方程就可以知道了

如果i<0 那么就没有价值,
如果i== 1 那么就是只有这件房屋的价值
其他情况就是max(case1,case2)
case1 = f(n-2)+A[n]
case2 = f(n-1)

暴力递归
Code

// 暴力递归
    private static int robWithDg(int[] nums,int high) {
        // base element
        if(high == 0){
            return nums[0];
        }
        if(high == 1){
            return Math.max(nums[0],nums[1]);
        }

        // high 需要
        int result = robWithDg(nums,high-2)+nums[high];
        result = Math.max(result,robWithDg(nums,high-1));
        return result;
    }

这里我就提前一位,也是一致的,熟悉自底向上的就可以一目了然

备忘递归
Code

private static int robWithDpWithRemember(int[] nums){
        int[] dp = new int[nums.length];
        Arrays.fill(dp,-1);
        return robWithDgWithRemember(nums,nums.length-1,dp);
    }

    // 备忘dp
    private static int robWithDgWithRemember(int[] nums,int high,int[] dp) {
        // dp取值
        if(dp[high]>-1){
            return dp[high];
        }

        // base element
        if(high == 0){
            dp[high] = 0;
            return nums[0];
        }
        if(high == 1){
            dp[high] = Math.max(nums[0],nums[1]);
            return dp[high];
        }

        // high 需要
        dp[high] = Math.max(robWithDgWithRemember(nums,high-2,dp)+nums[high],robWithDgWithRemember(nums,high-1,dp));
        return dp[high];
    }

自底向上
Code

// 自底向上
    private static int robWithDp(int[] nums){
        int[] dp = new int[nums.length];
        for(int i = 0;i<nums.length;i++){
            if(i == 0){
                dp[0] = nums[0];
                continue;
            }
            if(i == 1){
                dp[i] = Math.max(nums[0],nums[1]);
                continue;
            }

            dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[nums.length-1];
    }

如果你不了解上面的解法,请看上一篇文章
人人都会动态规划
其实到备忘递归就可以结束了。这里还是给出了自底向上的解法。
在这里插入图片描述
两者的差距也就是一点,这里可以考虑一下计算一下栈深度来计算时间复杂度。

打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:
输入:nums = [1,2,3]
输出:3

提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这道题和上面的题有异曲同工之妙,其实就是求解两个分问题作比较,提前不要第一个结点,或者不要最后一个节点,取结果最好的,为什么是这样的思路呢?很简单,因为第一个和第二个不能同时获得嘛。然后子问题就和第一问一致了。

这里就不墨迹了,直接最后版本
自底向上
Code

public static void main(String[] args) {
        Integer[] nums = (Integer[]) NumberArrayUtil.getArray(int.class,10);
        NumberArrayUtil.print(nums);

        for(int i = 0;i<10000;i++){
            nums = (Integer[]) NumberArrayUtil.getArray(int.class,10);
            int n1 = rob(nums);
            int n2 = robWithDg(nums);
            if(n1 != n2){
                System.out.println("测试失败");
                break;
            }
        }
        System.out.println("测试成功");
    }

    private static int robWithDg(Integer[] nums){
        if(nums.length == 1){
            return nums[0];
        }

        return Math.max(
                rob_(Arrays.copyOfRange(nums,0,nums.length-1),nums.length-2),
                rob_(Arrays.copyOfRange(nums,1,nums.length),nums.length-2)
        );
    }

    private static int rob(Integer[] nums){
        if(nums.length == 1){
            return nums[0];
        }

        return Math.max(
                robWithDp(Arrays.copyOfRange(nums,0,nums.length-1)),
                robWithDp(Arrays.copyOfRange(nums,1,nums.length))
        );
    }

    private static int rob_(Integer[] nums,int high){
        // base element
        if(high == 0){
            return nums[0];
        }
        if(high == 1){
            return Math.max(nums[0],nums[1]);
        }

        // high 需要
        int result = rob_(nums,high-2)+nums[high];
        result = Math.max(result,rob_(nums,high-1));
        return result;
    }

    // 自底向上
    private static int robWithDp(Integer[] nums){
        int[] dp = new int[nums.length];
        for(int i = 0;i<nums.length;i++){
            if(i == 0){
                dp[0] = nums[0];
                continue;
            }
            if(i == 1){
                dp[i] = Math.max(nums[0],nums[1]);
                continue;
            }

            dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[nums.length-1];
    }

这里融合了暴力递归和dp的写法,经过10000次测试是没有问题的。
在这里插入图片描述

打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:
树的节点数在 [1, 104] 范围内
0 <= Node.val <= 104
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/house-robber-iii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

上面有两张图,这里没法复制过来,所以可以通过这个链接直接对其求解

这里还是沿用上面的思路,以当前为节点能得到的最大价值设置函数,还是一样两种状态,一种拿,一种不拿。

拿的话就是 一个值加四个子问题的值,不拿就是两个子问题的值。

这里应该不需要解释。如果明白了第一问的话。
直接暴力递归
暴力递归
Code

// 暴力递归
    private static int rob(TreeNode root){
        if(root == null){
            return 0;
        }

        // 不要根结点
        int result = rob(root.left)+rob(root.right);

        // 要根节点
        int temp = root.val;
        if(root.left!=null){
            temp += rob(root.left.left)+rob(root.left.right);
        }
        if(root.right!=null){
            temp += rob(root.right.left)+rob(root.right.right);
        }
        result = Math.max(result,temp);

        return result;
    }

为什么会直接选择暴力递归,我当时认为反正我递归也是树结构,难道还需要记录吗,好家伙,这就是魔怔了。
想当然,这肯定超了。
那么怎么记录呢?看着给的实例,陷入沉思,是不是暗示我用数组描述呢。再看一下结点个数,应该是。那么好,带备忘的dp就来了
备忘递归
Code

private static int getLength(TreeNode root){
        if(root == null){
            return 0;
        }

        return Math.max(getLength(root.left),getLength(root.right))+1;
    }

    // 内存溢出
    private static int robWithRememberDg(TreeNode root){
        int len = getLength(root);
        int[] dp = new int[(int) Math.pow(2,len)];
        return robWithRememberDg(root,dp,1);
    }

    private static int robWithRememberDg(TreeNode root,int[] dp,int low){
        if(root == null){
            return 0;
        }

        if(dp[low]>0){
            return dp[low];
        }

        // 不要根结点
        int result = robWithRememberDg(root.left,dp,low*2)+robWithRememberDg(root.right,dp,low*2+1);

        // 要根节点
        int temp = root.val;
        if(root.left!=null){
            temp += robWithRememberDg(root.left.left,dp,low*2*2)+robWithRememberDg(root.left.right,dp,low*2*2+1);
        }
        if(root.right!=null){
            temp += robWithRememberDg(root.right.left,dp,(low*2+1)*2)+robWithRememberDg(root.right.right,dp,(low*2+1)*2+1);
        }
        result = Math.max(result,temp);

        dp[low] = result;
        return result;
    }

这次就是内存超了,后面想了一下,结点个数也可以在一条链上啊,所以这是必超的。
这个做不到,那么只能使用散列表了,Java中最好的散列表就是map,直接用对象成键,价值为值。这样就可以了。
备忘Dp
Code

// hashMap维护
    private static int robWithRememberHashDp(TreeNode root){
        HashMap<TreeNode,Integer> map = new HashMap<>();
        return robWithRememberHashDp(root,map);
    }

    private static int robWithRememberHashDp(TreeNode root,HashMap<TreeNode,Integer> map){
        if(root == null){
            return 0;
        }

        if(map.containsKey(root)){
            return map.get(root);
        }

        // 不要根结点
        int result = robWithRememberHashDp(root.left,map)+robWithRememberHashDp(root.right,map);

        // 要根节点
        int temp = root.val;
        if(root.left!=null){
            temp += robWithRememberHashDp(root.left.left,map)+robWithRememberHashDp(root.left.right,map);
        }
        if(root.right!=null){
            temp += robWithRememberHashDp(root.right.left,map)+robWithRememberHashDp(root.right.right,map);
        }
        result = Math.max(result,temp);

        map.put(root,result);
        return result;
    }

完成这里我们就结束了嘛。不不不,如果你了解树的动态规划就可以想到一些优化点,比如中序搜索树可以排序,后续可以自底向上等一些技巧。所以,自底向上的就来了。本来我以为就简单的到这也结束了,好家伙,跟着01背包的思路,空间也要砍,这样就可以得到最后一版本。
自底向上
Code

 // 后序遍历模拟动态规划
    public static int robh(TreeNode root) {
        int[] h = h(root);
        return h[0];
    }

    // 0位置是结点的最大价值 ,1位置表示不可偷
    public static int[] h(TreeNode node) {
        if (node == null) {
            return new int[2];
        }
        int[] left = h(node.left);
        int[] right = h(node.right);
        int todo = left[1] + right[1] + node.val;
        int undo = left[0] + right[0];
        return new int[]{Math.max(todo,undo),undo};
    }

上述代码省去了一个hash表维护的自底向上。而这个代码是网上大佬的,所以说,任重而道远。
在这里插入图片描述

贪心算法

想到贪心我就会想到找零问题,活动安排问题,霍夫曼编码,等一些问题,为什么呢,因为就这些特例了呀,其他都是套路。基本被归纳了。开整

求解最优化问题通常要设计一系列的步骤,而每个步骤需要涉及多种选择,对于许多最优化问题,使用动态规划就是大材小用,而可以想到使用简单暴力的贪心思路来完成。
贪心思路:每一步都选择局部最优,最后由局部最优组合成全局最优
当然,你必须的证明这是可行的。
这里我并不能归纳一些好的贪心思路,但是我i明白,贪心就得符合贪心选择性质。这也是我为什么没办法很好的描述贪心算法或者记录的原因。要明白这个思路,我认为,刷题就是必胜客。
又或者你能明白拟阵在贪心策略种的使用。这里还是以题目来开展。

活动选择问题

这也是一个经典的问题。

假定一个有n个活动 (activity)的集合S= {a1,a2,….,an},这些活动使用同一个资源(例如同一个阶梯教室),而这个资源在某个时刻只能供一个活动使用。 每个活动ai都有一个开始时间si和一个结束时间fi,其中0<=si<fi<正无穷。

如果这道题目你以前就明白,但是你知道他的原理吗?你明白为什么要这样做吗。

使用动态规划的思想来解决这个问题。

我需要选择一个活动划分两个子问题出来,这个子问题就会变成
从开始时间到这个活动开始时间安排最多活动
从这个活动结束时间到结束时间安排最多的活动
然后我需要遍历去寻找这个活动,而且要满足不影响的约束。这里的不影响表示活动的举行时间不会有冲突。那么左右两边加起来的最大就是我的值。
很显然,这里我们有两个变量,一个活动的开始时间i,一个活动的结束时间j,为什么是两个变量,明明就可以用一个变量表示啊,错,因为活动的市场是不同的。所以对于不同的活动也就是不同的值。
记录的值也就不需要解释了。

这里我们继续考虑,由活动k得到的解就是活动k的最大兼容集,也就是在k可以展开的前提下,可以最多兼容的活动的集合。
那如果使用偏激思想,我只需要这个活动结束后的最大兼容集呢?
所以,我希望这个活动结束后还有更多的时间去兼容其他活动。
那么我就应该以结束时间去升序排序,然后继续思考。
很好,这样这里的变量就成一个了,也就是当拿掉最早结束时间后的最大兼容集了。

这个思路就比动态规划的思路清晰了。
可能这里你还会有点懵,不过没关系。
在这里插入图片描述
这里面是否兼容需要开始时间和结束时间和最大时长来判断。
而贪心思路会是怎么样的呢?
在这里插入图片描述
所以这个问题就对换成为求最大兼容集的问题。这时候,你应该有一点贪心的思路,就是以局部上的贪心去替换复杂的动态选择,然后将变量降低处理。
相对的,这个问题也可以转换成最晚开始的问题。

事实上,求解最大兼容集和最小兼容集算法是很多问题的求解思路
比如区间着色问题。

区间着色问题

解法一:

以最小影响排序,然后求解他们的最大兼容集,在原问题集上减去这个兼容集,递归调用求解,直至问题集为空。

解法图示
在这里插入图片描述
该问题的变体种也就有一个活动安排场地问题,就是一系列活动中,要安排最少的场地举办这些活动。
我们可以使用图来建立是否兼容,因为这里没有介绍图,所以还是后话。

那么贪心基础就在这里结束了,后续我想想该记录什么吧,回溯,分支限界还是摊还分析。有点难搞。
还是那句话,刷题才是必胜客。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BoyC啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值