leetcode:198. 打家劫舍

题目来源

题目描述

在这里插入图片描述

class Solution {
public:
    int rob(vector<int>& nums) {

    }
};

在这里插入图片描述

题目解析

  • 动态规划的本质是递归,递归解决不了的问题,动态规划也解决不了

  • 所有的算法都是穷举,只是是不是聪明的穷举

暴力搜索

class Solution {
    /*
     * 从第一家开始抢(也可以从第一家最后一家开始抢,习惯性缩小问题的规模)
     * idx:表示抢到了第几家店
     * */
    int process(int idx, std::vector<int> & nums){
        int N = nums.size();
        if(idx == N){
            return 0;
        }
        
        if(idx == N - 1){
            return nums[idx];
        }

        /* 对于当前店,我们有两种决策
           *  * 抢: 得到了收益 nums[idx], 但是不能抢下一家店了
           *  * 不抢:可以抢下一家点
           *  从这两种决策中选一种最大的
           * */
        int p1 = process(idx + 1, nums);
        int p2 = nums[idx] + process(idx + 2, nums);
        return std::max(p1, p2);
    }
public:
    int rob(vector<int>& nums) {
        return process(0, nums);
    }
};
  • 时间复杂度分析:
    • 对于每一家店,有两种决策(每个点只抢一家店),一共有N个店,那么时间复杂度是2^N
    • 它会超时,优化一定是去掉冗余,用数组、map之类的存储

另外:

  • 暴力搜索一定是递归,不可能是迭代。
  • 暴力递归本质上是枚举,但是用迭代的话代码就太难写了,所以一般写成递归

思路

  • 假设当前来到了i,那么i有两种可能性:
    • 不抢
  • 如果抢,它会影响 i + 1 i + 1 i+1,但是不会影响 i i i之前的状态
  • 所以我们假定 [ 0... i − 1 ] [0...i-1] [0...i1]都被解决了。
  • 问题是 i i i能不能抢呢?因为 [ 0... i − 1 ] [0...i-1] [0...i1]不会影响 i i i,所以它可以自由选择抢还是不抢。只是这个抢还是不抢会影响 i + 1 i+1 i+1而已

记忆化搜索(记忆化搜索可以看出是动态规划)

为什么要上备忘录

因为这道题所有数都是整数,所以要尽可能的抢,才能得到最佳效率

  • 假设当前抢了n --> 那么下一家可能是(n + 2,n + 3,n + 4…)
  • 假设当前抢了n --> 那么下一家可能是(n + 3,n + 4…)

从上面,可以看出会大量重复计算,也就是n + 3,n + 4…

为什么这道题不能用贪心算法解决。

  • 因为这道题有后滞性。
    • 我们抢了第n家店,那么第n+1家店就不能抢了
    • 所以只能dp,不能贪心(贪心问题必须天生无后效性)
  • dp是怎么处理后效性的?(dp的本质是递归,递归不能解决的问题dp一定不能解决)
    • 后效性就是:
      • 当抢了第n家店,那么第n+1家店就不能抢了
      • 也就是说做了当前决策之后,会影响后面的决策
        • 没有做当前决策之前,每家店都能抢
        • 做了当前决策之后,不是每家店都能抢了
    • 从上面递归代码可以看出
      • 当抢了nums[idx],下一个问题就缩小为solve(idx + 2, nums),跳过了idx + 1 ----> nums[idx] + solve(idx + 2, nums) —> 也就是说,将[后效性]处理成了[无后效性]
      • 当不抢nums[idx]时,本来就[无后效性],所以问题缩小为solve(idx + 1, nums)

两个概念

  • 最优子结构

    • 子问题最优决策可以导出原问题最优决策
    • 无后效性:如果原问题有后效性,应该将之改造成无后效性
  • 重叠子问题

    • 需要解决重叠子问题,也就是说去冗余:
    • 一般方法是:空间换时间
  • 套路:

    • 题目中出现“最优、最大、最小、最长、计数”之类的
  • 动态规划一般都是离散问题

    • 比如背包重量都是整数时,就是01背包;如果背包是小数,那么就悬
    • 为什么必须是离散:因为小数不容易穷举和存储

代码

class Solution {
    std::vector<int> result;
    int process(int idx, std::vector<int> & nums){
        int N = nums.size();
        if(idx == N){
            return 0;
        }

        if(idx == N - 1){
            return nums[idx];
        }

        if(result[idx] >= 0){  // 不是初始化状态,说明一定处理过了
            return result[idx];
        }

        result[idx] = std::max(
                process(idx + 1, nums),
                nums[idx] + process(idx + 2, nums)
                );
        return result[idx];
    }
public:
    int rob(vector<int>& nums) {
        result.resize(nums.size());
        for (int i = 0; i < result.size(); ++i) {
            result[i] = -1;  // 因为要求最大值
        }
        return process(0, nums);
    }
};

这就是dp,也叫做记忆化搜索。

  • 时间复杂度: O[N](每家店只计算一次)
  • 空间复杂度:O[N]

序列型动态规划

思路一:将记忆化搜索改为动态规划

class Solution {
    std::vector<int> result;

public:
    int rob(vector<int>& nums) {
        if(nums.empty()){
            return 0;
        }
        
        if(nums.size() == 1){
            return nums[0];
        }

        result.resize(nums.size());
        for (int i = 0; i < result.size(); ++i) {
            result[i] = -1;  // 因为要求最大值
        }

        result[0] = nums[0];  // 只有一家店,那就只能强这一家了
        result[1] = std::max(nums[0], nums[1]); // 有两家店,那就抢钱多的那一家
        for (int idx = 2; idx < nums.size(); ++idx) {
            result[idx] = std::max(
                    nums[idx] + result[idx - 2],
                    result[idx - 1]
            );
        }


        return result[nums.size() - 1];
    }
};

思路二:将暴力递归改为动态规划

(1)第一步:准备一张表

  • 问题是,这张表是一维的还是二维的,应该多大。我们可以通过分析递归函数的可变参数有几个,其变化范围多大来决定
 int process(int idx, std::vector<int> & nums)
  • 可变参数为:idx,其变化范围是0~N
  • 因为只有一个可变参数,所以dp应该是一维数组;因为变化范围是0~n,所以数组长度为n+1。也就是:
 std::vector<int> dp(n + 1)

(2)确定返回值

  • 怎么确定呢?通过看主函数是怎么调用递归函数的
   return process(0, nums);
  • 可以看出,可变参数初始为0,所以应该返回dp[0]

(3)填表

(3.1)先初始化表,也就是看base case

        int N = nums.size();
        if(idx == N){
            return 0;
        }
        
        if(idx == N - 1){
            return nums[idx];
        }

  • 因此初始化: d p [ n ] = 0 , d p [ n − 1 ] = n u m s [ i d x ] dp[n] = 0,dp[n - 1] = nums[idx] dp[n]=0dp[n1]=nums[idx]

(3.2) 再分析普通情况,这个时候要分析清楚依赖关系

        int p1 = process(idx + 1, nums);
        int p2 = nums[idx] + process(idx + 2, nums);
        return std::max(p1, p2);
  • 可以看出,它主要依赖右边两个表格的值,因此,应该从右往左填写
   	for (int idx = n - 2; idx >= 0; --idx) {
      
        }
  • 置于怎么填,照着上面依赖关系抄就行了

综上:

class Solution {

public:
    int rob(vector<int>& nums) {
        if(nums.empty()){
            return 0;
        }
        int n = nums.size();
        std::vector<int> dp(n + 1);
        dp[n] = 0;
        dp[n - 1] = nums[n - 1];
        for (int idx = n - 2; idx >= 0; --idx) {
            int p1 = dp[idx + 1];
            int p2 = dp[idx + 2] + nums[idx];
            dp[idx] = std::max(p1, p2);
        }
        return dp[0];
    }
};

美团面试题:

题目描述

给定一个数组arr,在不能取相邻数的情况下,返回所有组合中的最大累加和

题目解析

就是打家劫舍,代码是一样的

定义: dp[i]为[0…i]范围上不相邻的情况下,怎么选取能够得到最大累加和

思路

思考:0~i范围上得到的最好累加和的可能性有哪些?

  • 只要这个数时,累加和最大
  • 不要这个数时,累加和最大
    • 因为不要这个数 a r r [ i ] arr[i] arr[i] [ 0 , i − 1 ] [0,i-1] [0i1]范围上怎么选那么就选择选
  • 既要这个数,也要左边的数时,累加和最大
    • 因为要这个数 a r r [ i ] arr[i] arr[i],那么相邻的 a r r [ i − 1 ] arr[i-1] arr[i1]就不能选,左边只能在 [ 0 , i − 2 ] [0,i-2] [0i2]范围上选择

特殊情况:

  • 当只有一个数时
  • 当只有两个数时

返回值:dp[N-1]:表示在[0…N-1]范围上不相邻的情况下,怎么选取能够得到最大累加和

int rob(std::vector<int>& nums){
    if(nums.empty() ){
        return 0;
    }

    int N = nums.size();

    if(N == 1){
        return nums[0];
    }

    if(N == 2){
        return std::max(nums[0], nums[1]);
    }

    std::vector<int> dp(N);
    dp[0] = nums[0];
    dp[1] = std::max(nums[0], nums[1]);
    for (int i = 2; i < N; ++i) {
        dp[i] = std::max(nums[i], std::max(dp[i - 1], nums[i] + dp[i - 2]));
    }
    return dp[N-1];
}

类似题目

题目思路
algorithm:派对的最大快乐值
leetcode:198. 打家劫舍(一排) House Robber
leetcode:213. 打家劫舍(环形数组) II House Robber II
leetcode:337. 打家劫舍(二叉树) III House Robber III
leetcode:740. 删除并获得点数 Delete and Earn
leetcode:968. 监控二叉树 binary-tree-cameras
leetcode:53. 子数组最大累加和 Maximum Subarray
leetcode:152. 子数组最大乘积 Maximum Product Subarray
leetcode:238. 除自身以外数组的乘积 Product of Array Except Self
leetcode:256. 粉刷房子Paint House
leetcode:265. 粉刷房子 II Paint House II
leetcode:276. 粉刷篱笆 Paint Fence
600. 不含连续1的非负整数 Non-negative Integers without Consecutive Ones
656. 金币路径 Coin Path
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值