Leetcode_打家劫舍三道题(动态规划总结)

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

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

示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400

思路:动态规划
分为以下四个步骤

  • 定义子问题
  • 写出子问题的递推关系
  • 确定 DP 数组的计算顺序
  • 空间优化(可选)

首先定义子问题,题目是求偷n间房间的最大值,分解成子问题即求k个房间的最大值f(k),又保证不能相邻,则递归分析规律,f(k)要么第k个不偷 要么偷第k个 不偷第k-1个
其动态转移方程就为f(k)=max(f(k-1),f(k-2)+nums[k-1])

int rob(vector<int>& nums) {
    if (nums.size() == 0) {
        return 0;
    }

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

然后就是空间优化,很多时候我们并不需要始终持有全部的 DP 数组。对于小偷问题,我们发现,最后一步计算 f(n)f(n) 的时候,实际上只用到了 f(n-1)f(n−1) 和 f(n-2)f(n−2) 的结果。n-3n−3 之前的子问题,实际上早就已经用不到了。那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题

int rob(vector<int>& nums) {
    int prev = 0;
    int curr = 0;

    // 每次循环,计算“偷到当前房子为止的最大金额”
    for (int i : nums) {
        // 循环开始时,curr 表示 dp[k-1],prev 表示 dp[k-2]
        // dp[k] = max{ dp[k-1], dp[k-2] + i }
        int temp = max(curr, prev + i);
        prev = curr;
        curr = temp;
        // 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
    }

    return curr;
}

打家劫舍II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

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

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

思路:打家劫舍2 这次相比较于1,由直线变成了环,也就是说第一位和嘴和一位也算相邻的,所以就考虑三种情况,1.最后一个和第一个都没偷2.偷第一个没偷最后一个3.偷最后一个没偷第一个 而求最大值 第一种情况显然比后两种更小,所以只用考虑后两种求max 定义二维数组 dp[][2],当两次线性路动态规划求解,最后求出最大值

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 0){
            return 0;
        }else if(n < 4){
            return (*max_element(nums.begin(),nums.end()));
        }
        int dp[n][2];               //边界情况:
        dp[0][0] = 0;               //没偷第0家时,偷到前0家的最大金额为0
        dp[0][1] = nums[0];         //有偷第0家时,偷到前0家的最大金额为nums[0]
        dp[1][1] = nums[0];         //有偷第0家时,偷到前1家的最大金额为nums[0]
        dp[1][0] = nums[1];         //没偷第0家时,偷到前1家的最大金额为nums[1]
        for(int i = 2; i < n; i++){
            if(i != n - 1){
                //如果不是偷最后一家,分有偷第0家和不偷第0家两条路线进行状态转移
                dp[i][0] = max(dp[i-1][0],dp[i-2][0] + nums[i]);
                dp[i][1] = max(dp[i-1][1],dp[i-2][1] + nums[i]);
            }else{
                //偷最后一家时,如果没偷过第0家,可以直接选择偷或者不偷
                dp[i][0] = max(dp[i-1][0],dp[i-2][0] + nums[i]);
                //如果已经偷过第0家,只能选择不偷
                dp[i][1] = dp[i-1][1];
            }
        }
        //返回两种偷法中金额较大的一种
        return max(dp[n-1][0],dp[n-1][1]);
    }
};

java优化方案

class Solution {
    public int rob(int[] nums) {
        if(nums.length == 0) return 0;
        if(nums.length == 1) return nums[0];
        return Math.max(myRob(Arrays.copyOfRange(nums, 0, nums.length - 1)), 
                        myRob(Arrays.copyOfRange(nums, 1, nums.length)));
    }
    private int myRob(int[] nums) {
        int pre = 0, cur = 0, tmp;
        for(int num : nums) {
            tmp = Math.max(pre + num, cur);
            pre = cur;
            cur=tmp;
        }
        return cur;
    }
}

打家劫舍III

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

示例 1:
输入: [3,2,3,null,3,null,1]
Leetcode_打家劫舍三道题(动态规划总结)_第1张图片
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:

输入: [3,4,5,1,3,null,1]
Leetcode_打家劫舍三道题(动态规划总结)_第2张图片
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.

我们可以用 f(o) 表示选择 o 节点的情况下,o节点的子树上被选择的节点的最大权值和;g(o)表示不选择 o 节点的情况下,o节点的子树上被选择的节点的最大权值和;l和 r 代表 o 的左右孩子。

当 o 被选中时,o 的左右孩子都不能被选中,故 o 被选中情况下子树上被选中点的最大权值和为 l 和 r 不被选中的最大权值和相加,即 f(o) = g(l) + g®

当 o 不被选中时,o 的左右孩子可以被选中,也可以不被选中。对于 o 的某个具体的孩子 x,它对 o 的贡献是 x 被选中和不被选中情况下权值和的较大值。故 g(o) =max { f(l) , g(l)}+max{ f® , g®}

至此,我们可以用哈希映射来存 f 和 g 的函数值,用深度优先搜索的办法后序遍历这棵二叉树,我们就可以得到每一个节点的 f 和 g。根节点的 f 和 g 的最大值就是我们要找的答案。

/**
 * 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 {
public:
    unordered_map <TreeNode*, int> f, g;

    void dfs(TreeNode* root) {
        if (!root) {
            return;
        }
        dfs(root->left);
        dfs(root->right);
        f[root] = root->val + g[root->left] + g[root->right];
        g[root] = max(f[root->left], g[root->left]) + max(f[root->right], g[root->right]);
    }

    int rob(TreeNode* root) {
        dfs(root);
        return max(f[root], g[root]);
    }
};

你可能感兴趣的:(Leetcode,#,动态规划,动态规划,leetcode,算法)