【算法】动态规划(三)——打家劫舍系列问题

目录

一、前言

 二、打家劫舍

(1)198. 打家劫舍Ⅰ

• 整体代码:

(2)213. 打家劫舍 II

• 题目分析

• 整体代码:

(3)337. 打家劫舍Ⅲ

• 思路分析

• 整体代码:

三、补充知识——fmax && fmin

Summery💐💐💐


一、前言

经过之前对动态规划的学习,相信大家对解题步骤和题目分析的技能已经有了很大的提升,接下来我们一起学习动态规划的另一个系列问题

打家劫舍:这系列问题总体围绕着相邻不偷的原则,求最后能偷到的最大金额,这类问题首先想到的肯定是用动态规划的方法,接下来我会用两三道题带大家一同体会!✔️

 二、打家劫舍

(1)198. 打家劫舍Ⅰ

leetcode传送➡️https://leetcode.cn/problems/house-robber/

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

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

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

1. dp数组定义

通过读题,很快就能明确dp数组的含义,及偷到第i间房子所得最大金额,最后返回dp[numsSize-1]即可;

2. 递推公式 

对于第 i 间房子,有偷或者不偷两种选择:

选择,那么第 i-1 间房子就不能投,第 i-2 间房子可以偷,此时dp[i] = dp[i-2] + nums[i];

如果选择不偷,那么第 i-1 间房子就可以偷了,此时dp[i] = dp[i-1];

最终比较偷与不偷的最大金额即可

3. 初始化

由上述递推公式可知,结合dp数组的含义,dp[0] = nums[0];dp[1] = max(nums[0], nums[1]);

4. 遍历顺序

这道题不讲究遍历顺序,可从前往后也可从后往前,道理是一样的。

• 整体代码:

#define max(x,y) (x) > (y) ? (x) : (y) 

int rob(int* nums, int numsSize) {
    //特殊情况,只有一间房间时:
    if (numsSize == 1)
        return nums[0];
    //初始化
    int dp[numsSize];
    memset(dp, 0, sizeof(dp));
    dp[0] = nums[0];
    dp[1] = max(nums[0], nums[1]);
    //递推
    for (int i = 2; i < numsSize; i++)
    {
        dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
    }

    return dp[numsSize - 1];
}

(2)213. 打家劫舍 II

leetcode传送➡️https://leetcode.cn/problems/house-robber-ii/

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

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

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

• 题目分析

由题目可知,房子围成一周,意味着第一个房子和最后一个房子肯定是不能一起偷的;

所以我们可以想到将首尾分开讨论,这样既避开了首位同时偷的情况,又避免了重复缺漏;

最后分别用打家劫舍Ⅰ的方法来求出两种偷法的最大金额

 

1. dp数组定义 

同打家劫舍Ⅰ一样,dp数组的含义依然是偷到第i间房子所得最大金额,最后返回含首和含尾两者取的较大值

2. 递推公式 && 初始化 && 遍历顺序

递推公式、初始化 以及遍历顺序与打家劫舍Ⅰ相同,有不理解的同学可以到上一题回顾;

• 整体代码:

#define max(x,y) (x) > (y) ? (x) : (y) 
int rob(int* nums, int numsSize) {
    //排除特殊情况
    if (numsSize == 1)
        return nums[0];
    if (numsSize == 2)
        return max(nums[0], nums[1]);
    //初始化含首dp
    int dp_h[numsSize];
    memset(dp_h, 0, sizeof(dp_h));
    dp_h[0] = nums[0];
    dp_h[1] = max(nums[0], nums[1]);
    //遍历dp_h
    for (int i = 2; i < numsSize - 1; i++)
    {
        dp_h[i] = max(dp_h[i - 2] + nums[i], dp_h[i - 1]);
    }

    //初始化含尾dp
    int dp_t[numsSize];
    memset(dp_t, 0, sizeof(dp_t));
    dp_t[1] = nums[1];
    dp_t[2] = max(nums[1], nums[2]);
    //遍历dp_t
    for (int i = 3; i < numsSize; i++)
    {
        dp_t[i] = max(dp_t[i - 2] + nums[i], dp_t[i - 1]);
    }


    return max(dp_h[numsSize - 2], dp_t[numsSize - 1]);
}

(3)337. 打家劫舍Ⅲ

leetcode传送➡️https://leetcode.cn/problems/house-robber-iii/

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 2:

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

• 思路分析

 • 读完题我们大概能知道这道题的题设,相邻结点不能偷,但这道题涉及到了二叉树的概念,如果大家对二叉树不是很了解,可以去我之前的文章了解二叉树的基本知识和三种遍历顺序https://blog.csdn.net/Dusong_/article/details/127061544?spm=1001.2014.3001.5502• 这道题我们从二叉树的根结点开始看,如果偷根结点,那么它的两个子结点就不能偷;如果不偷根结点,那么他的左右结点可以偷也可以不偷

• 也就是说,要判断该结点偷不偷,就需要知道它左右子结点偷与不偷所获得的最大钱币,及这时我们需要用后序遍历(遍历顺序)的方法遍历二叉树!

int* left_dp = robTree(cur->left);   //递归左子树,返回值放入dp数组中
int* right_dp = robTree(cur->right);   //递归右子树,返回值放入dp数组中

1. dp数组定义

与上面两道题不同的是,对于一个结点我们需要他偷和不偷两个最大值,而不是取偷与不偷中一个较大值(我认为根本原因是二叉树不是线性表,不能像前两道题一样在for循环里递推)

所以我们需要在每一个结点定义一个dp数组,

dp[0]表示不偷该结点所获最大金额

dp[1]表示偷该结点所获最大金额

因为为一层递归都会在栈区开辟一个空间,所以每层递归栈区里都会保存该结点的dp数组,出函数会销毁,所以最后将dp数组返回到上一结点即可。

2. 递推公式 

由上述可知:

如果不偷根结点,那么他的左右结点可以偷也可以不偷⬇️

dp[0] = fmax(left_dp[0], left_dp[1]) + fmax(right_dp[0], right_dp[1]);

如果偷根结点,那么它的两个子结点就不能偷⬇️

 dp[1] = left_dp[0] + right_dp[0] + cur->val;

• 整体代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */
int* robTree(struct TreeNode* cur)
{ 
    //递归到空结点,说明该结点偷与不偷都为0,返回{0,0}即可
    if (cur == NULL)
    {
        int* dp = (int*)malloc(sizeof(int) * 2);
        dp[0] = 0;
        dp[1] = 0;
        return dp;
    }
    //后序遍历
    int* left_dp = robTree(cur->left);   //递归左子树
    int* right_dp = robTree(cur->right);   //递归右子树

    int* dp = (int*)malloc(sizeof(int) * 2);
    //不偷该结点
    dp[0] = fmax(left_dp[0], left_dp[1]) + fmax(right_dp[0], right_dp[1]);
    //偷该结点
    dp[1] = left_dp[0] + right_dp[0] + cur->val;
    return dp;
}

int rob(struct TreeNode* root) {
    int* ret = (int*)malloc(sizeof(int) * 2);
    ret = robTree(root);
    return fmax(ret[0], ret[1]);
}

Summery💐💐💐

到这里leetcode上三道打家劫舍的问题就已经解决完了,希望大家有所收获👌

这篇文章制作还是比较粗糙,希望大家见谅,也非常感谢能阅读到这里的同学!🙏

动态规划所涉及的问题广泛且多样,希望我们能共破难关!

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Dusong_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值