动态规划--Day03--打家劫舍--198. 打家劫舍,213. 打家劫舍 II,2320. 统计放置房子的方式数

动态规划–Day03–打家劫舍–198. 打家劫舍,213. 打家劫舍 II,2320. 统计放置房子的方式数

今天要训练的题目类型是:【打家劫舍】,题单来自@灵艾山茶府

掌握动态规划(DP)是没有捷径的,咱们唯一能做的,就是投入时间猛猛刷题。

动态规划要至少刷100道才算入门!

记忆化搜索是新手村神器。方便理解,写完之后可以转译成递推。

但是有些题目只能写递推,才能优化时间复杂度。熟练之后直接写递推也可以。

198. 打家劫舍

方法:记忆化搜索(递归)

思路:

和爬楼梯几乎是同一个模板的。爬楼梯是memo[i] = res1 + res2;,而打家劫舍是memo[i] = Math.max(res1, res2);

后来忽然发现了一个细微的差别,爬楼梯,缓存一般是开n+1位,而打家劫舍缓存一般开n位就够了。

  1. 使用memo记忆,缓存已经探索过的节点
  2. 从数组最后一个值开始往前探索
    • 递归结束判断:如果索引超出范围,则返回
    • 如果memo有值,用缓存值
    • 对于当前探索的i这个节点,能不能偷,有两个情况:
      • 情况一:偷i-1家,那么i家就不能偷(递归进去探索i-1节点)
      • 情况二:偷i-2家,那么i家也能偷(递归进去探索i-2节点)
      • 返回两种情况的较大值,顺便保存到记忆
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;

        // 记忆(缓存),默认未使用标志为-1
        int[] memo = new int[n];
        Arrays.fill(memo, -1);

        // 从索引n-1开始搜索(也就是数组的最后一个值)
        return dfs(n - 1, nums, memo);
    }

    private int dfs(int i, int[] nums, int[] memo) {
        // 索引超出范围,返回
        if (i < 0) {
            return 0;
        }

        // 如果已经判断过这种情况了,直接用记忆(缓存)
        if (memo[i] != -1) {
            return memo[i];
        } else {

            // 情况一:偷i-1家,那么i家就不能偷
            // 情况二:偷i-2家,那么i家也能偷
            int res1 = dfs(i - 1, nums, memo);
            int res2 = dfs(i - 2, nums, memo) + nums[i];

            // 返回两种情况的较大值,顺便保存到记忆
            memo[i] = Math.max(res1, res2);
        }
        return memo[i];
    }
}

从dfs函数来看,一进去先判断索引是否超出范围,超出了则返回。其实是浪费多了一次资源(因为调用函数要压栈,出栈等,时间空间都变慢了)

可以改成这样:先判断索引,再考虑调用函数:

private int dfs(int i, int[] nums, int[] memo) {
    if (memo[i] != -1) {
        return memo[i];
    } else {
        
        // 先判断索引合法,再调用函数
        int res1 = 0;
        if (i - 1 >= 0) {
            res1 = dfs(i - 1, nums, memo);
        }

        // res2要赋值为nums[i]
        int res2 = nums[i];
        if (i - 2 >= 0) {
            res2 = dfs(i - 2, nums, memo) + nums[i];
        }

        memo[i] = Math.max(res1, res2);
    }
    return memo[i];
}

注意,这里res2要默认赋值为nums[i]。因为这是一个决策,是决定偷i-2,那么i家先偷了,再去i-2,然后发现i-2没有人住(超出索引范围),那么i家我也偷到了。

如果写成这样是错的:

private int dfs(int i, int[] nums, int[] memo) {
    if (memo[i] != -1) {
        return memo[i];
    } else {
        
        int res1 = 0;
        int res2 = 0;
        if (i - 1 >= 0) {
            res1 = dfs(i - 1, nums, memo);
        }
        if (i - 2 >= 0) {
            res2 = dfs(i - 2, nums, memo) + nums[i];
        }

        memo[i] = Math.max(res1, res2);
    }
    return memo[i];
}

方法:动态规划(递推)

思路:

递推要初始化,因为f[i]需要依赖f[i-1]和f[i-2],所以要初始化前两个数,还要特判n==1的情况。

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        // 如果只有一家,直接偷,返回。
        if (n == 1) {
            return nums[0];
        }
        int[] f = new int[n];

        // 递推要初始化,因为f[i]需要依赖f[i-1]和f[i-2],所以要初始化前两个数
        f[0] = nums[0];
        f[1] = Math.max(nums[0], nums[1]);

        // 从第三个数开始遍历
        for (int i = 2; i < n; i++) {

            // 情况一:偷i-1家,那么i家就不能偷
            // 情况二:偷i-2家,那么i家也能偷
            int res1 = f[i - 1];
            int res2 = f[i - 2] + nums[i];

            // 偷较大值
            f[i] = Math.max(res1, res2);
        }
        // 最后的结果,在最后一家判断完之后,包含了所有的情况。
        // 所以答案在最后一个索引的位置。
        return f[n - 1];
    }
}

思路:

把f数组索引直接+2。上一篇代码的f[0]的值,放到这一题代码的f[2]的位置。

不管f[0]和f[1],直接不用他们。

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        int[] f = new int[n + 2];
        for (int i = 0; i < n; i++) {
            f[i + 2] = Math.max(f[i + 1], f[i] + nums[i]);
        }
        return f[n + 1];
    }
}

思路:

空间优化的写法。

class Solution {
    public int rob(int[] nums) {
        int f0 = 0;
        int f1 = 0;
        for (int x : nums) {
            int newF = Math.max(f1, f0 + x);
            f0 = f1;
            f1 = newF;
        }
        return f1;
    }
}

这些都是写完之后的优化版本,如果第一次写,不要想复杂的,先按照自己内心第一思路,把题目写完先,再想优化。

213. 打家劫舍 II

方法:动态规划(递推)

思路:

  • 特判索引0位置,把剩余位置变为非循环数组进行操作。
  • 索引0位置只有两种选择,偷或者不偷。
    • 偷索引0,那么索引1和索引n-1不能偷。问题变为从索引2到n-2的非循环数组。
    • 不偷索引0。问题变为从索引1到n-1的非循环数组。
  • 取二者较大值
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        // 偷索引0,那么索引1和索引n-1不能偷。问题变为从索引2到n-2的非循环数组。
        int res1 = nums[0] + rob1(nums, 2, n - 2);

        // 不偷索引0。问题变为从索引1到n-1的非循环数组。
        int res2 = rob1(nums, 1, n - 1);

        // 取二者较大值
        return Math.max(res1, res2);
    }

    // 上一题的空间优化版本
    private int rob1(int[] nums, int start, int end) {
        int f0 = 0;
        int f1 = 0;
        // [start,end] 左闭右闭
        for (int i = start; i <= end; i++) {
            int newF = Math.max(f1, f0 + nums[i]);
            f0 = f1;
            f1 = newF;
        }
        return f1;
    }
}

2320. 统计放置房子的方式数

方法:动态规划(递推)

思路:

  • 两边互相独立,求一边,最后f[n] * f[n]即可。
  • 初始化f[0]和f[1]。如果没有空地,只有不放这1种选择;如果只有一块空地,有放和不放2种选择。
  • 对于每个f[i]:
    • 情况一:如果不放f[i]的话,f[i-1]可放可不放
    • 情况二:如果放f[i]的话, f[i-2]可放可不放
    • 两种情况加起来,就是f[i]可放可不放
  • 最后返回结果时:
    • 注意,两个MOD相加,不会溢出int。但是两个MOD相乘,会溢出int
    • 所以要先转为long,相乘,取模,后再转为int
class Solution {
    public int countHousePlacements(int n) {
        // 两边互相独立,求一边,最后f[n] * f[n]即可
        final int MOD = 1000000007;
        int[] f = new int[n + 2];

        // 初始化。如果没有空地,只有不放这1种选择;如果只有一块空地,有放和不放2种选择。
        f[0] = 1;
        f[1] = 2;
        for (int i = 2; i <= n; i++) {

            // 情况一:如果不放f[i],f[i-1]可放可不放
            // 情况二:如果放f[i], f[i-2]可放可不放
            // 两种情况加起来,就是f[i]可放可不放
            f[i] = (f[i - 1] + f[i - 2]) % MOD;
        }

        // 注意,两个MOD相加,不会溢出int。但是两个MOD相乘,会溢出int
        // 所以要先转为long,相乘,取模,后再转为int
        return (int) ((long) f[n] * f[n] % MOD);
    }
}

总感觉这道题跟《打家劫舍》没什么关系,反而像是《爬楼梯》。每个位置都可爬可不爬,然后结果加起来。f[i] = (f[i - 1] + f[i - 2])。初步是这么想,多做题,回头再看看。

关于取模运算,可以看@灵艾山茶府的这篇文章:模运算的世界:当加减乘除遇上取模

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值