跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • [高频题]dp 经典题目 House Robber
未分類
20 3 月 2021

[高频题]dp 经典题目 House Robber

[高频题]dp 经典题目 House Robber

資深大佬 : youthes 4

不得不说,刷题已经和爬山、溜娃一样,成为湾区三俗,基本几个湾区的工程师碰在一起,讨论的话题总跳不出这个圈。爬山,哦不,刷题作为一个贯穿码农整个职业生涯的必须品(就算是我目前呆的微软谷歌这种养老公司也总得跳一跳,毕竟雪花的大包裹是真香啊),几年来基本每天不间断的刷,算是对这一块略有心得。帖子的前半部分想分享一些我作为面试官的常出的一些经典题目,以及题目思路的解析以及一些同类题的归纳,帖子的后半部分我会参考坛友们的留言和拍砖,来决定后面的走向

帖子会长期保持更新,只要是带娃的间隙就会偷偷上来更一下,尽量保持每周两更。

由于太多人加我 VX,我也创建了一个算法工作交流群,大家感兴趣可以加我拉你进群

我的 VX:sxxzs3998 不管是具体的题目讨论还是给帖子的建议和拍砖,或者想进群都欢迎添加(记得加我要备注 v2 )

本期我们分享 dp 经典题目–打家劫舍问题
打家劫舍系列总共有三道题目,层层递进。第一道是比较标准的动态规划问题,而第二道融入了环形数组的条件,第三道则让盗贼在二叉树上打劫

大佬有話說 (5)

  • 主 資深大佬 : youthes

    House Robber I
    你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
    给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

     
    示例 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 。

  • 主 資深大佬 : youthes

    解决动态规划问题就是找「状态」和「选择」。

    从左到右走过这一排房子,在每间房子前都有两种选择:抢或者不抢。

    如果你抢了这间房子,那么你肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。

    如果你不抢这间房子,那么你可以走到下一间房子前,继续做选择。

    当你走过了最后一间房子后,你就没得抢了,能抢到的钱显然是 0 ( base case )。

    以上的逻辑很简单吧,其实已经明确了「状态」和「选择」:你面前房子的索引就是状态,抢和不抢就是选择。

    在两个选择中,每次都选更大的结果,最后得到的就是最多能抢到的 money:
    // 主函数
    public int rob(int[] nums) {
    return dp(nums, 0);
    }
    // 返回 nums[start..] 能抢到的最大值
    private int dp(int[] nums, int start) {
    if (start >= nums.length) {
    return 0;
    }

    int res = Math.max(
    // 不抢,去下家
    dp(nums, start + 1),
    // 抢,去下下家
    nums[start] + dp(nums, start + 2)
    );
    return res;
    }

    明确了状态转移,就可以发现对于同一`start`位置,是存在重叠子问题的
    盗贼有多种选择可以走到这个位置,如果每次到这都进入递归,所以说存在重叠子问题,可以用备忘录进行优化:
    private int[] memo;
    // 主函数
    public int rob(int[] nums) {
    // 初始化备忘录
    memo = new int[nums.length];
    Arrays.fill(memo, -1);
    // 强盗从第 0 间房子开始抢劫
    return dp(nums, 0);
    }

    // 返回 dp[start..] 能抢到的最大值
    private int dp(int[] nums, int start) {
    if (start >= nums.length) {
    return 0;
    }
    // 避免重复计算
    if (memo[start] != -1) return memo[start];

    int res = Math.max(dp(nums, start + 1),
    nums[start] + dp(nums, start + 2));
    // 记入备忘录
    memo[start] = res;
    return res;
    }

    这就是自顶向下的动态规划解法,我们也可以略作修改,写出自底向上的解法:
    int rob(int[] nums) {
    int n = nums.length;
    // dp[i] = x 表示:
    // 从第 i 间房子开始抢劫,最多能抢到的钱为 x
    // base case: dp[n] = 0
    int[] dp = new int[n + 2];
    for (int i = n – 1; i >= 0; i–) {
    dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]);
    }
    return dp[0];
    }

    我们又发现状态转移只和 dp[i]最近的两个状态有关,所以可以进一步优化,将空间复杂度降低到 O(1)
    int rob(int[] nums) {
    int n = nums.length;
    // 记录 dp[i+1] 和 dp[i+2]
    int dp_i_1 = 0, dp_i_2 = 0;
    // 记录 dp[i]
    int dp_i = 0;
    for (int i = n – 1; i >= 0; i–) {
    dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
    dp_i_2 = dp_i_1;
    dp_i_1 = dp_i;
    }
    return dp_i;
    }

  • 主 資深大佬 : youthes

    House Robber II

    这道题目和第一道描述基本一样,强盗依然不能抢劫相邻的房子,输入依然是一个数组,但是告诉你这些房子不是一排,而是围成了一个圈。

    也就是说,现在第一间房子和最后一间房子也相当于是相邻的,不能同时抢。比如说输入数组`nums=[2,3,2]`,算法返回的结果应该是 3 而不是 4,因为开头和结尾不能同时被抢。

  • 主 資深大佬 : youthes

    首先,首尾房间不能同时被抢,那么只可能有两种不同情况:

    + 第一间房间被不被抢无所谓,最后一间不被抢( robRange(nums, 0, n – 2))
    + 第一间不被抢,最后一间被不被抢无所谓( robRange(nums, 1, n – 1))

    所以只需对之前的解法稍作修改即可:
    public int rob(int[] nums) {
    int n = nums.length;
    if (n == 1) return nums[0];
    return Math.max(robRange(nums, 0, n – 2),
    robRange(nums, 1, n – 1));
    }

    // 仅计算闭区间 [start,end] 的最优结果
    int robRange(int[] nums, int start, int end) {
    int n = nums.length;
    int dp_i_1 = 0, dp_i_2 = 0;
    int dp_i = 0;
    for (int i = end; i >= start; i–) {
    dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
    dp_i_2 = dp_i_1;
    dp_i_1 = dp_i;
    }
    return dp_i;
    }

  • 主 資深大佬 : youthes

    House Robber III

    第三题又想法设法地变花样了,此强盗发现现在面对的房子不是一排,不是一圈,而是一棵二叉树!房子在二叉树的节点上,相连的两个房子不能同时被抢劫:

    示例 1:

    输入: [3,2,3,null,3,null,1]

    3
    /
    2 3

    3 1

    输出: 7
    解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
    示例 2:

    输入: [3,4,5,1,3,null,1]

      3
    /
    4 5
    /
    1 3 1

    输出: 9
    解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.

    整体的思路完全没变,还是做抢或者不抢的选择,取收益较大的选择。甚至我们可以直接按这个套路写出代码:

    Map<TreeNode, Integer> memo = new HashMap<>();
    public int rob(TreeNode root) {
    if (root == null) return 0;
    // 利用备忘录消除重叠子问题
    if (memo.containsKey(root))
    return memo.get(root);
    // 抢,然后去下下家
    int do_it = root.val
    + (root.left == null ?
    0 : rob(root.left.left) + rob(root.left.right))
    + (root.right == null ?
    0 : rob(root.right.left) + rob(root.right.right));
    // 不抢,然后去下家
    int not_do = rob(root.left) + rob(root.right);

    int res = Math.max(do_it, not_do);
    memo.put(root, res);
    return res;
    }

    这道题就解决了,时间复杂度 O(N),`N`为数的节点数。

    这样,打家劫舍系列问题就全部解决了,其实也没多难吧

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具