动态规划:打家劫舍系列

LeetCode198 打家劫舍I

https://leetcode-cn.com/problems/house-robber/

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

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

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

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i]表示在nums[0 … i]中能够偷到的最高金额

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

决定dp[i]的因素就是第i房间偷还是不偷:

  • 如果偷第i房间,那么第i-1房间一定是不考虑的,在nums[0 … i - 2]的房屋中偷窃最多可以偷窃的金额为dp[i-2] ,再加上第i房间偷到的钱。即dp[i] = dp[i - 2] + nums[i] 。
  • 如果不偷第i房间,那么考虑i-1房。(注意这里是考虑,并不是一定要偷i-1房)。即dp[i] = dp[i - 1] 。

显然在以上两种情况中取最大值,所以:dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

3.思考初始状态

dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])

4.自底向上计算得到最优解

        for (int i = 2; i < size; ++i)
        {
            dp[i] = max(dp[i - 2] + nums[i] , dp[i - 1]);
        }

5.思考是否可以进行空间的优化

可以仅维护两个数值,不需要记录整个序列。

        int pre = nums[0];
        int cur = max(nums[0], nums[1]);
        for (int i = 2; i < size; ++i)
        {
            int temp = max(pre + nums[i], cur);
            pre = cur;
            cur = temp;
        }
代码
class Solution 
{
public:
    //动态规划
    /*int rob(vector<int>& nums) 
    {
        int size = nums.size();
        if (size == 0)
        {
            return 0;
        }
        if (size == 1)
        {
            return nums[0];
        }

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

        return dp[size - 1];
    }*/

    //优化动态规划
    int rob(vector<int>& nums) 
    {
        int size = nums.size();
        if (size == 0)
        {
            return 0;
        }
        if (size == 1)
        {
            return nums[0];
        }

        int pre = nums[0];
        int cur = max(nums[0], nums[1]);
        for (int i = 2; i < size; ++i)
        {
            int temp = max(pre + nums[i], cur);
            pre = cur;
            cur = temp;
        }

        return cur;
    }
};

LeetCode213. 打家劫舍II

https://leetcode-cn.com/problems/house-robber-ii/

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

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

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
思路

本题和LeetCode198 打家劫舍是差不多的,唯一区别就是成环了。

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素
  • 情况二:考虑包含首元素,不包含尾元素
  • 情况三:考虑包含尾元素,不包含首元素

注意这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素。

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。

可以简单的复用LeetCode198 打家劫舍的代码。

代码
class Solution 
{
private:
    int robHelper(const vector<int>& nums, int left, int right) 
    {
        int pre = nums[left];
        int cur = max(nums[left], nums[left + 1]);
        for (int i = left + 2; i <= right; ++i)
        {
            int temp = max(pre + nums[i], cur);
            pre = cur;
            cur = temp;
        }

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

        int result1 = robHelper(nums, 0, size - 2);
        int result2 = robHelper(nums, 1, size - 1);

        return max(result1, result2);
    }
};

LeetCode337. 打家劫舍III

https://leetcode-cn.com/problems/house-robber-iii/

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

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

     3
    / \
   2   3
    \   \ 
     3   1

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

首先转换问题:一棵二叉树,树中每个节点带有权值且树中每个节点有选和不选两种状态。如果选择双亲节点就不能选左右孩子节点,则选择的所有节点权值之和最大是多少。

本题为树形动态规划问题。

1.确定递归函数的参数和返回值

题意要求得到一个节点偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

vector<int> dfs(TreeNode* cur)

该返回值数组就是dp数组:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。在递归的过程中,系统栈会保存每一层递归的参数,所以长度为2的数组可以标记树中每个节点的状态。

2.确定终止条件

在遍历的过程中,如果遇到空节点的话,很无论偷还是不偷都是0。

        if (cur == NULL)
        {
            return vector<int>{0, 0};
        }

这也相当于dp数组的初始化

3.确定遍历顺序

首先明确的应使用后序遍历,因为要通过递归函数的返回值来做下一步计算。

通过递归左节点,得到左节点偷与不偷的金钱。

通过递归右节点,得到右节点偷与不偷的金钱。

        vector<int> left = dfs(cur -> left);
        vector<int> right = dfs(cur -> right);

4.确定单层递归的逻辑

如果不偷当前节点,那么左右孩子就可以考虑偷,注意是考虑,至于到底偷不偷一定是选一个最大的,

所以:result[0] = max(left[0], left[1]) + max(right[0], right[1])

如果是偷当前节点,那么左右孩子就不能偷,

所以:result[1] = cur->val + left[0] + right[0]

        vector<int> result(2);
        result[0] = max(left[0], left[1]) + max(right[0], right[1]);
        result[1] = result[1] = cur->val + left[0] + right[0];

这也相当于dp数组的推导过程

代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
 
class Solution 
{
private:
    //0代表不偷,1代表偷
    vector<int> dfs(TreeNode* cur)
    {
        if (cur == NULL)
        {
            return vector<int>{0, 0};
        }

        vector<int> left = dfs(cur -> left);
        vector<int> right = dfs(cur -> right);
        vector<int> result(2);
        result[0] = max(left[0], left[1]) + max(right[0], right[1]);
        result[1] = result[1] = cur->val + left[0] + right[0];

        return result;
    }
public:
    int rob(TreeNode* root) 
    {
        vector<int> result = dfs(root);

        return max(result[0], result[1]);
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值