198. House Robber 关于动态规划问题(Dynamic programming)的学习、思考与记录

动态规划(Dynamic programming)

定义

任何数学递推公式都可以直接转换为递归算法,但是基本现实是编译器常常不能正确对待递归算法,结果导致低效的程序。当怀疑出现这样的情况时,我们必须再给编译器提供一些帮助,即将递归算法转换为非递归算法,让后者把那些子问题的答案系统的记录在一个表内。利用这种方法的一种技巧叫做动态规划。

以LeetCode.198. House Robber为例进行一下算法的优化。

大多数的问题可以按照下面的顺序进行解决:
1.找到递归关系,得到递归关系式。
2.自顶向下的递归。(top-down)
3.自顶向下的递归 + 存储记录。(top-down)
4.转化为非递归 + 存储记录。(自下向上 bottom-up)
5.非递归 + n 个变量存储(bottom-up)

第一步:找到递归关系。
题中盗贼有两个选择:
(1)洗劫当前的房子 i;(2)不洗劫当前的房子。
若选(1)则意味着他不能洗劫前一个房子i-1,但是他可以对前面的i-2个房子进行有选择的洗劫。
若选(2)则意味着他可以对前面的i-1个房子进行有选择的洗劫。

因此递归关系为:

rob(i) = Math.max( rob(i - 2) + currentHouseValue, rob(i - 1) )

第二步:自顶向下的递归。(top-down)
将第一步的递归关系式转换如下:

public int rob(int[] nums) {
    return rob(nums, nums.length - 1);
}
private int rob(int[] nums, int i) {
    if (i < 0) {
        return 0;
    }
    return Math.max(rob(nums, i - 2) + nums[i], rob(nums, i - 1));
}

由于上述算法会重复的处理相同的子问题导致计算复杂度会随输入规模快速增长,问题多一个输入,那么计算的规模就要乘上2,所以时间复杂度为2^n这个级别,所以此方案放在leetcode上会超时,因此需要优化。

第三步:自顶向下的递归 + 存储记录。(top-down)

class Solution {
    int[] memo;
    public int rob(int[] nums) {
        memo = new int[nums.length];
        Arrays.fill(memo, -1);
        return rob(nums, nums.length - 1);
    }

    private int rob(int[] nums, int i) {
        if (i < 0) {
            return 0;
        }
        //这一步的判断可以防止重复计算相同的子问题
        if (memo[i] >= 0) {
            return memo[i];
        }
        int result = Math.max(rob(nums, i - 2) + nums[i], rob(nums, i - 1));
        memo[i] = result;
        return result;
    }
}

这个算法就好很多了,时间复杂度和空间复杂度都为O(n),下一步优化可以试着摆脱递归堆栈。

第四步:转化为非递归 + 存储记录。(自下向上 bottom-up)
主体还是上面提到的那个关系,只是我们将子问题的结果放在了一个临时数组中。临时数组中存放的都是子问题的最优解。比如memo [1]里面存放的就是前两个元素中最大值,即最优解。当计算前三个数中最优解的时候,我只要做一个选择,即要不要选择nums[2]这个元素,选择的话,那么我就从memo [2-2]中得到最优解,加起来就是当前最优解,不选择的话,就从memo [2-1]中选择最优解。依次下去,tmp中最后一个值就是整个序列中组合的最优解了。

这里注意一下,这里其实是记忆化搜索的思想来实现的,我们可以注意到,其实是自顶向下来看的,从第一个数字来一直推到最后。然而动态规划的思想是从底向上的,参见第一个递归版本的实现。我们先考虑的是最终的n,而不是考虑从0开始。所以在设计思想上是有所区别的,但是又是非常类似,有的人将他们归位一类,我想,它们在大多数场景下可以互换的化,可以认为都是广义上的DP算法吧,因为DP毕竟只是一种思想,正过来实现反过来实现也未尝不可。

我们用一个数组来存放子问题的最优解,大大降低了时间复杂度,leetcode上也顺利通过。其实这个是一种记忆化搜索的思想,上面这个用了一个数组,其实完全没有必要用数组,用两个变量即可。

class Solution {
    public int rob(int[] nums) {
        if(nums.length <= 0){
            return 0;
        }
        if(nums.length == 1){
            return nums[0];
        }
        int[] memo = new int[nums.length];
        memo[0] = nums[0];
        memo[1] = nums[0] > nums[1] ? nums[0] : nums[1];
        for(int i=2;i<nums.length;i++){
            int A = memo[i-2] + nums[i];
            int B = memo[i-1];
            int max = A > B ? A : B;
            memo[i] = max;
        }
        return memo[nums.length-1];
    }
}

第五步:.非递归 + n 个变量存储(bottom-up)

class Solution {
    public int rob(int[] nums) {
        if(nums.length <= 0){
            return 0;
        }
        if(nums.length == 1){
            return nums[0];
        }
        
        int a = nums[0];
        int b = nums[0] > nums[1] ? nums[0] : nums[1];
        for(int i=2;i<nums.length;i++){
            int A = a + nums[i];
            int B = b;
            int max = A > B ? A : B;
            a = b;
            b = max;
        }
        return b;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值