Leetcode 198.打家劫舍
题目链接:198 打家劫舍
题干:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
1 <= nums.length <= 100
0 <= nums[i] <= 400
思考:动态规划。难点:当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷。
- 确定dp数组(dp table)以及下标的含义
dp[i]:下标i(包括i)以内的房屋,偷窃的最高金额。
- 确定递推公式
决定dp[i]的因素就是第i房间偷还是不偷。
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房。
并且dp[i]取最大值,则dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
- dp数组如何初始化
从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]
从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
- 确定遍历顺序
由于dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历。
- 例推导dp数组
举例:[2,7,9,3,1]。dp状态如图:
代码:
class Solution {
public:int rob(vector<int>& nums) {if (nums.size() == 0) return 0; //情况一:数组长度为0if (nums.size() == 1) return nums[0]; //情况二:数组长度为1vector<int> dp(nums.size()); //下标i(包括i)以内的房屋,偷窃的最高金额dp[0] = nums[0];dp[1] = max(nums[0], nums[1]);for (int i = 2; i < nums.size(); i++) //遍历房间dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);return dp[nums.size() - 1]; }
};
Leetcode 213.打家劫舍II
题目链接:213 打家劫舍II
题干:你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
1 <= nums.length <= 100
0 <= nums[i] <= 1000
思考:动态规划。本题和上题的唯一区别就是成环。
对于一个数组,成环的话主要有如下三种情况:
- 情况一:考虑不包含首尾元素
- 情况二:考虑包含首元素,不包含尾元素
- 情况三:考虑包含尾元素,不包含首元素
情况二 和 情况三 都包含了情况一,所以只需要用上题的处理逻辑考虑情况二和情况三即可,返回最大值。
代码:
class Solution {
public://打家劫舍的逻辑int robRange(const vector<int>& nums, int start, int end) {if (start == end) return nums[start]; //打劫区间为1vector<int> dp(nums.size()); //下标i(包括i)以内的房屋,偷窃的最高金额dp[start] = nums[start];dp[start + 1] = max(nums[start], nums[start + 1]);for (int i = start + 2; i <= end; i++) //遍历房间dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);return dp[end]; }int rob(vector<int>& nums) {if (nums.size() == 0) return 0; //情况一:数组长度为0if (nums.size() == 1) return nums[0]; //情况二:数组长度为1int result1 = robRange(nums, 0, nums.size() - 2); //情况三:不考虑最后的房间int result2 = robRange(nums, 1, nums.size() - 1); //情况四:不考虑首个房间return max(result1, result2);}
};
Leetcode 337.打家劫舍III
题目链接:337 打家劫舍III
题干:小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为
root
。除了
root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。给定二叉树的
root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
- 树的节点数在
[1, 104]
范围内0 <= Node.val <= 104
思考一:递归法 + 暴力法。终止条件:如果当前节点为空,则返回0。如果当前节点为叶子节点,则不用处理直接返回此节点值即可。
单层递归逻辑:考虑两种情况。情况一:偷根节点,由于相邻节点不能都偷,因此递归处理左右孩子节点的孩子节点,并累加值、记录到val1。情况二:不偷根节点,则递归处理左右孩子节点,并累加值,记录到val2。取val1和val2的较大值返回。
按以上方式处理会超时,因此要添加备忘录,用于记录每个子树盗取的最高金额。当然单层递归逻辑要将上面的最大值记录下。此外终止条件要加上如果备忘录记录过,则返回记录数据。
代码:
class Solution {
public:unordered_map<TreeNode*, int> memo; //备忘录 记录每个子树盗取的最高金额int rob(TreeNode* root) {if (!root) return 0; //空节点返回0if (!root->left && !root->right) return root->val; //叶子节点返回节点值if (memo[root]) return memo[root]; //备忘录中添加过则直接返回数据//偷根节点int val1 = root->val;if (root->left) val1 += rob(root->left->left) + rob(root->left->right); //不偷左孩子节点if (root->right) val1 += rob(root->right->left) + rob(root->right->right); //不偷右孩子节点//不偷根节点int val2 = rob(root->left) + rob(root->right);memo[root] = max(val1, val2); //记录数据return memo[root];}
};
思考二:递归法 + 动态规划。动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
下面以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解思路
- 确定递归函数的参数和返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。其实就是dp数组,dp[0]表示不偷当前节点的情况,dp[1]表示偷当前节点的情况。这些dp数组保存在系统栈中,递归返回时使用。
- 确定终止条件
在遍历的过程中,如果遇到空节点的话,无论偷还是不偷都是0。这也是动态规划的初始化过程。
- 确定遍历顺序
由于处理完节点后, 栈中保存的dp数组数据在返回时才使用,因此使用后序遍历处理二叉树。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
- 确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; {不偷当前节点得到的最高金额,偷当前节点得到的最高金额}
这也是动态规划确定递推公式的过程。
- 举例推导dp数组
以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导)
最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱。
代码:
class Solution {
public:// 长度为2的数组,0:不偷,1:偷vector<int> robTree(TreeNode* cur) {if (!cur) return vector<int>{0, 0};vector<int> left = robTree(cur->left); //左vector<int> right = robTree(cur->right); //右int val1 = cur->val + left[0] + right[0]; //偷当前节点int val2 = max(left[0], left[1]) + max(right[0], right[1]); //不偷当前节点return vector<int>{val2, val1};}int rob(TreeNode* root) {vector<int> result = robTree(root);return max(result[0], result[1]);}
};
自我总结:
了解树形动态规划。感受递归+动态规划的设计逻辑:利用系统栈传递dp数组,相当于刷新dp数组
关于为什么树形结构,进行动态规划处理比进行暴力法处理节省时间以及为什么不会爆栈,先留下疑问,等理解清晰回头写思路。