【LeetCode】LeetCode之打家劫舍Ⅱ——暴力递归+动态规划解决循环问题+DP空间优化

这道题和第 198 题相似,建议读者首先阅读「198. 打家劫舍」

🔒LeetCode之打家劫舍ⅠLeetCode之打家劫舍Ⅰ


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 = [0]
输出:0

📜提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

2.打家劫舍Ⅱ思路分析

🔗直达打家劫舍①链接,默认读者已经搞懂了打家劫舍①.

该问题是在打家劫舍①的基础上,添加了一个循环结构。如果你已经理解了打家劫舍①,那么这个Ⅱ只需要考虑怎么解决这个循环问题即可,也就是第一个数据和最后一个数据不能同时出现,要么最大金额只包含第一个数据,要么只包含最后一个数据。

在这里插入图片描述

所以我们可以将数组分为两部分,各自找出每一部分的最大金额,然后返回它们两个金额最大的即可【你会发现这样分为两部分之后,就不会出现第一个和最后一个数据同时出现的问题了;然后每一部分的解题过程就和打家劫舍1一模一样了。


3.暴力解法-递归实现

📕流程

(1)将数组分为两部分即可

  • 0 ~ n - 1
  • 1 ~ n

(2)然后将每一部分按照打家劫舍Ⅰ求解即可
(3)返回这两部分结果的最大值

    public int rob(int[] nums) {
        int l = nums.length;
        if (l == 1) {
            return nums[0];
        }
        if (l == 2) {
            return Math.max(nums[0], nums[1]);
        }
        int[] preNums = new int[l - 1];
        int[] sufNums = new int[l - 1];
        for (int i = 0; i < l - 1; i++) {
            preNums[i] = nums[i];
            if (i > 0) {
                sufNums[i - 1] = nums[i];
            }
        }
        sufNums[l - 2] = nums[l - 1];
        //以上代码是将数组分为两个子数组【1~n,0~n-1】
        preNums[1] = Math.max(preNums[0], preNums[1]);
        sufNums[1] = Math.max(sufNums[0], sufNums[1]);
		//以上两行与打家劫舍1类似
        return Math.max(recursion(preNums.length - 1, preNums), recursion(sufNums.length - 1, sufNums));
    }
    public int recursion(int n, int[] nums) {
        if (n == 0 || n == 1) {
            return nums[n];
        }
        return Math.max(recursion(n - 1, nums), recursion(n - 2, nums) + nums[n]);
    }

当我将该暴力解法提交到LeetCode,发现会超时。
请添加图片描述
在这里插入图片描述
从上图我们可以看出使用递归出现了大量的重复计算(我只是枚举前三行,越往后重复越多),这是递归比较忌讳的问题之一(重复和栈溢出)。这样无异于在浪费CPU的运算单元。

复杂度分析:
空间复杂度:O(n)【每个栈空间复杂度为O(1)】O(n*1)=O(n)
时间复杂度:O(n^n)

  • 递归算法的空间复杂度与所生成的最大递归树的深度成正比。如果递归算法的每个函数调用都占用 O(m) 空间,并且如果递归树的最大深度为 n,则递归算法的空间复杂度将为 O(n·m)。”

💌上述中递归解题方式的缺点就是重复量比较多,所以我们可以可以将途中计算的结果保存下来,俗称记忆集或记忆化搜索。此时你应该想到这不正是动态规划嘛。


4.记忆化搜索-动态规划

需要两个dp数组进行保存,因为在抽象层面分为两个数组了嘛,所以要使用两个记忆集。其他就和打家劫舍1一样了。

循环体内部包含两部分

  • 0 ~ n-1
  • 1 ~ n
    public int rob2(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }
        if (nums.length == 2) {
            return Math.max(nums[0], nums[1]);
        }
        int[] dp1 = new int[nums.length - 1];
        dp1[0] = nums[0];
        dp1[1] = Math.max(nums[0], nums[1]);
        int[] dp2 = new int[nums.length - 1];
        dp2[0] = nums[1];
        dp2[1] = Math.max(nums[1], nums[2]);
        for (int i = 2; i < nums.length - 1; i++) {
        	//0~n-1
            dp1[i] = Math.max(dp1[i - 1], dp1[i - 2] + nums[i]);
            //1~n
            dp2[i] = Math.max(dp2[i - 1], dp2[i - 2] + nums[i + 1]);
        }
        return Math.max(dp1[nums.length - 2], dp2[nums.length - 2]);
    }

复杂度分析

空间复杂度为O(n)
时间复杂度为O(n)

当我提交LeetCode上时,可以发现通过了。请添加图片描述
分析一下:能不能将空间复杂度优化成O(1)呢?


5.动态规划之优化空间复杂度

如果理解了打家劫舍1和前面的流程,这个应该很简单。

    public int rob3(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }
        if (nums.length == 2) {
            return Math.max(nums[0], nums[1]);
        }
        int pre1 = nums[0], pre2 = nums[1];
        int suf1 = Math.max(nums[0], nums[1]);
        int suf2 = Math.max(nums[1], nums[2]);
        int maxMoney1 = suf1;
        int maxMoney2 = suf2;
        for (int i = 2; i < nums.length - 1; i++) {
            maxMoney1 = Math.max(pre1 + nums[i], suf1);
            pre1 = suf1;
            suf1 = maxMoney1;
            maxMoney2 = Math.max(pre2 + nums[i + 1], suf2);
            pre2 = suf2;
            suf2 = maxMoney2;
        }
        return Math.max(maxMoney1, maxMoney2);
    }

复杂度分析

时间复杂度:O(n)
空间复杂度:O(1)

提交到LeetCode,当然能通过

请添加图片描述
在这里插入图片描述

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Thecoastlines

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

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

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

打赏作者

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

抵扣说明:

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

余额充值