力扣是一共有三道关于打家劫舍的题,是学习动态规划很好的范例,总结一下。
第一题:
1.题目:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
题目链接:198.打家劫舍
2.思路:
采用动态规划的方法。
(1)将原问题分解为子问题:对于只有1间房的情况,结果为偷窃该房屋;对于有两间房的情况,应该选择2间房中存在金额更大的那一间房。对于房子数大于2的情况,如有k间房(k>2),分为偷窃第i间房和不偷窃第i间房两种情况。
(2)状态定义:
dp[i]表示共有i间房可以获得的最大金额。
(3)状态转移方程:
若有k(k>2)间房,
存在两种情况:
1.偷窃第i间房,则不能偷窃第i-1间房,偷窃金额为前i-2间房最高总金额之和和当前房金额之和。
2.不偷窃第i间房,偷窃金额为前i-1间房最高总金额之和。
{
d
p
[
i
]
=
d
p
[
i
−
2
]
+
n
u
m
s
[
i
]
(
偷
窃
第
i
间
房
)
d
p
[
i
]
=
d
p
[
i
−
1
]
(
不
偷
窃
第
i
间
房
)
.
\begin{cases} dp[i]=dp[i-2]+nums[i](偷窃第i间房) \\ dp[i] = dp[i-1](不偷窃第i间房) \end{cases}.
{dp[i]=dp[i−2]+nums[i](偷窃第i间房)dp[i]=dp[i−1](不偷窃第i间房).
(4)边界条件:
即只存在1间房或是0间房的情况。
{
d
p
[
0
]
=
n
u
m
s
[
0
]
d
p
[
1
]
=
m
a
x
(
n
u
m
s
[
0
]
,
n
u
m
s
[
1
]
)
.
\begin{cases} dp[0]=nums[0] \\ dp[1]=max(nums[0],nums[1]) \end{cases}.
{dp[0]=nums[0]dp[1]=max(nums[0],nums[1]).
3.代码:
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];
}
};
改进空间复杂度,将dp数组用滚动数组替代:
class Solution {
public:
int rob(vector<int>& nums) {
int size = nums.size();
if (size==0) return 0;
if (size==1) return nums[0];
//上一次存储的dp值
int dp_0 = nums[0];
//当前dp值
int dp_1 = max(nums[0],nums[1]);
//需要一个临时变量做交换使用
int dp_tmp;
for (int i = 2;i < size;i++)
{
dp_tmp = max(dp_0+nums[i], dp_1);
dp_0 = dp_1;
dp_1 = dp_tmp;
}
return dp_1;
}
};
第二题:
题目:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
题目链接:213.打家劫舍 II
思路:
这里其他的部分和第一题是一样的,主要是如何可以加入首尾不能同时选择的这个约束条件。
这里采用了分类讨论的方法,
- 选择首房间,结果在[0,size-1]中选择最大金额。
- 选择尾房间,结果在[1,size]中选择最大金额。
- 取二者结果的更大者。
这个思路还是不好想到,其实这种方法的应用还比较广泛,分治的思想。
class Solution {
public:
int rob_helper(vector<int>& nums, int start, int end)
{
int dp_0 = nums[start];
int dp_1 = max(nums[start], nums[start+1]);
int dp_tmp = 0;
for (int i = start+2;i < end;i++)
{
dp_tmp = max(dp_0+nums[i], dp_1);
dp_0 = dp_1;
dp_1 = dp_tmp;
}
return dp_1;
}
int rob(vector<int>& nums) {
int size = nums.size();
//房屋树小于2间时不需要考虑首尾不能同时选择的约束
if (size == 1) return nums[0];
if (size == 2) return max(nums[0], nums[1]);
//将选择首尾的问题分类讨论
//1.只选择首房间,范围为0,size-1
int res1 = rob_helper(nums,0,size-1);
//2.只选择尾房间,范围为1,size
int res2 = rob_helper(nums,1,size);
//选择两种情况中的更大者
return max(res1, res2);
}
};
第三题:
1.题目:
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
原题链接:337.打家劫舍 III
2.思路:
该题目为动态规划的树形版本。
法一:
最朴素的思路:
时间负责度很高,大部分节点会被重复计算。
class Solution {
public:
int rob(TreeNode* root) {
if (root == nullptr) return 0;
//计算选择当前节点和四个孩子结点的最大金额
int res = root->val;
if (root->left != nullptr)
{
res += rob(root->left->right)+rob(root->left->left);
}
if (root->right != nullptr)
{
res += rob(root->right->right)+rob(root->right->left);
}
//选择当前节点和四个孩子的孩子结点的最大金额与两个孩子结点最大金额中的更大者
return max(res, rob(root->left)+rob(root->right));
}
};
法二:
时间复杂度很高没有进行记忆,进行了多次重复计算每个节点所获得的最大金额的值。
用哈希表进行记忆,
class Solution {
private:
unordered_map<TreeNode*, int> hash;
public:
int rob(TreeNode* root) {
if (root == nullptr) return 0;
if (hash.count(root)) return hash[root];
//计算选择当前节点和四个孩子结点的最大金额
int res = root->val;
if (root->left != nullptr)
{
res += rob(root->left->left)+rob(root->left->right);
}
if (root->right != nullptr)
{
res += rob(root->right->left)+rob(root->right->right);
}
//选择当前节点和四个孩子的孩子结点的最大金额与两个孩子结点最大金额中的更大者
int result = max(res, rob(root->left)+rob(root->right));
hash[root] = result;
return result;
}
};
法三:
分为两种情况:
- 选择当前节点,则不能选择该节点的左孩子结点和右孩子结点。
- 不选择当前节点,可以选择该节点的左孩子结点和右孩子结点,也可以不选择在二者中取更大者。
这里主要思路上的转变是不止用一个dp变量来保存当前结果,而引入了left和right作为左右子树的结果。
3.代码:
class Solution {
public:
vector<int> dfs(TreeNode* root)
{
if (root == nullptr) return {0,0};
vector<int> left = dfs(root->left);
vector<int> right = dfs(root->right);
vector<int> dp(2,0);
//不选择当前节点,结果为左孩子和右孩子结果的最大值
dp[0] = max(left[0], left[1]) + max(right[0],right[1]);
//选择当前节点,结果为当前节点结果
dp[1] = root->val + left[0] + right[0];
return dp;
}
int rob(TreeNode* root) {
//树形结构的二叉树
vector<int> res = dfs(root);
return max(res[0], res[1]);
}
};