LeetCode题解 打家劫舍Ⅰ -- 动态规划入门

281 阅读4分钟

一、题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例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.带备忘的自顶向下法

  • 此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常在一个数组或者散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间,否则,按通常方式计算子问题。
  • 简单来说,就是在递归的过程中保存每次递归的值。
  • 回到题目,对房屋的数目进行递归:设在前k个房屋里最多能偷到的钱数目为f(k),则分为两种情况:对第k间房屋进行盗窃,以及不对第k间房屋进行盗窃。前者所能得到的金额为nums[k]+f(k-2),即第k间房屋所拥有的金额和前k-2间房子所能得到的最大金额(不能偷相连在一起的房间);后者所能得到的金额为f(k-1),即前k-1间房子里所能得到的最多金额。
  • 显然f(k)=max(f(k-1),f(k-2)+nums[k]).
  • 代码如下
class Solution {
    public int rob(int[] nums) {
        int []money = new int[nums.length+1];
        for(int i=0;i<money.length;i++)money[i]=-1;
        return robByRecur(nums.length,money,nums); 
    }
    //money[i]为从前i间房内可以偷取的最大金额
    int robByRecur(int size,int []money,int []nums){
        if(size<=0)return 0;
        if(money[size]>=0)
            return money[size];
        int m2 = robByRecur(size-1,money,nums);
        int m1 = nums[size-1]+robByRecur(size-2,money,nums);
        int max = m1>m2?m1:m2;
        money[size] = max;
        return max;
    }
}
  • robByRecur(i,money,nums)函数返回从 前i间房间内可以偷到的最大金额数
  • 在递归的过程中用money数组记录每次递归的结果,money[i]即从前i间房间内可以偷到的最大金额数。
  • 其中:robByRecur(size-1,money,nums)与robByRecur(size-2,money,nums)存在重复递归的部分(前者是从size-1递归到0,后者是从size-2递归到0,重复了size-2到0这一系列的递归过程),利用money数组的特点:已经计算过的前n间房中所能偷到的最大金额数>=0,未计算过的前n间房中所能偷到的最大金额数等于-1,当money[size]>=0时递归可以直接返回,从而可以有效避免递归重复所造成的时间上的浪费。

2.自底向上法。

  • 此方法一般需要恰当的定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题求解。因而我们可以将子问题按规模排序,由小至大的顺序进行求解。
  • 简单来说,就是递归的逆过程,已知f(k-1)和f(k-2),求f(k).
  • f(k) = max( f(k-1) , f(k-2) + nums[k] ).
class Solution {
    public int rob(int[] nums) {
        int []epochs = new int[nums.length+1];
        for(int i=0;i<epochs.length;i++)epochs[i]=0;
        for(int size=1;size<=nums.length;size++){
            int m1 = nums[size-1] ;
            if(size-2>0)
                m1 += epochs[size-2];
            int m2 = epochs[size-1];
            epochs[size] = m1>m2?m1:m2;
        }
         return epochs[nums.length];
    }
}
  • epochs[i]和money[i]的意义相同,为前i间屋子里可以偷到的最多金额。
  • 遍历到末尾,epochs[nums.length]即前n间屋子中可以偷到的最多金额总数。

总结:动态规划两种常用方法

1.自顶向下:在递归的同时保存每次递归的结果,如果遇到已经求解过的子问题,则直接将保存的结果拿来使用,避免重复计算。

2.自底向上:先解决所有的子问题,并保存结果;利用子问题的解来解决最终问题。