本文主要记录力扣的打家劫舍专题,涉及到普通的一维DP,树形DP,以及DP和二分的组合。本文所有代码可在我的算法题目收录仓库找到。
198. 打家劫舍
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路
根据题目描述,该题可以看作 求数组的最大不相邻子序列和 问题
采用选或不选的原则,状态转换方程是:
d
p
[
0
]
=
n
u
m
s
[
0
]
d
p
[
1
]
=
m
a
x
(
d
p
[
0
]
,
n
u
m
s
[
1
]
)
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
,
d
p
[
i
−
2
]
+
n
u
m
s
[
i
]
)
,
i
≥
2
dp[0] = nums[0] \\ dp[1] = max(dp[0], nums[1])\\ dp[i] = max(dp[i-1], dp[i-2]+nums[i]),i\geq{2}
dp[0]=nums[0]dp[1]=max(dp[0],nums[1])dp[i]=max(dp[i−1],dp[i−2]+nums[i]),i≥2
代码
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==1)return nums[0];
else if(nums.size()==2)return max(nums[0], nums[1]);
int first = nums[0], second = max(nums[1], nums[0]); // second 代表到该下标为止,能够得到的最高金额
for(int i=2;i<nums.size();i++){
int tmp = second;
second = max(second, first+nums[i]);
first = tmp;
}
return second;
}
};
213. 打家劫舍 II
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
思路
与上题相比,多了一个首尾不能同时选的限制,那么设想,在上题的情况下,遍历到dp[nums.size()-1]
时,其无非由两种情况转化,而不管是哪种,都有可能是dp[0]转化而来,也就是偷了首家,那么如何避免这种情况呢,把首家去掉,遍历时从第二家开始即可[1,nums.size()-1]。同理把最后一家去掉再遍历[0,nums.size()-2]
代码
class Solution {
public:
int robRange(vector<int>& nums, int start, int end) {
int dp[110] = {0};
dp[start] = nums[start];
dp[start+1] = max(dp[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) {
int length = nums.size();
if (length == 1) {
return nums[0];
} else if (length == 2) {
return max(nums[0], nums[1]);
}
return max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
};
337. 打家劫舍 III
题目描述
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
思路
要求相连接的两个节点不能同时偷。
这题就是非常典型的树形DP了。最初的思想是从root开始往下递推(正向思维)求dp值。基本思路就是使用BFS搜索,计算每层的总金额,相邻层不能同时偷。dp[i]代表到第i层为止的最大可偷金额,是遵循把树压扁成数组的形式。
这种方法会出现状态不全,比如下图中两个红色节点是相邻层,可以同时偷,并且偷的金额最大,显然根据BFS的思想是不会考虑这种情况的。
树形DP很多情况就用来解决不同子树之间正向递推时无法进行状态转化的问题。因为树形DP是反向思维,从根节点开始向上递推。
考虑每个节点作为根节点的子树中最大可偷金额。每个节点可有偷或不偷的选择。采用dfs思想,每次递归返回一个二元组 ( d p 偷 r o o t , d p 不偷 r o o t ) (dp_{偷}^{root}, dp_{不偷}^{root}) (dp偷root,dp不偷root)。
二元组转换方程为:
d
p
偷
r
o
o
t
=
d
p
不偷
r
o
o
t
−
>
l
e
f
t
+
d
p
不偷
r
o
o
t
−
>
r
i
g
h
t
d
p
不偷
r
o
o
t
=
m
a
x
(
d
p
不偷
r
o
o
t
−
>
l
e
f
t
,
d
p
偷
r
o
o
t
−
>
l
e
f
t
)
+
m
a
x
(
d
p
不偷
r
o
o
t
−
>
r
i
g
h
t
,
d
p
偷
r
o
o
t
−
>
r
i
g
h
t
)
dp_{偷}^{root} = dp_{不偷}^{root->left}+dp_{不偷}^{root->right}\\ dp_{不偷}^{root} = max( dp_{不偷}^{root->left},dp_{偷}^{root->left})+max( dp_{不偷}^{root->right},dp_{偷}^{root->right})
dp偷root=dp不偷root−>left+dp不偷root−>rightdp不偷root=max(dp不偷root−>left,dp偷root−>left)+max(dp不偷root−>right,dp偷root−>right)
代码
class Solution {
public:
unordered_map<TreeNode*, pair<int, int>> us;
void robdfs(TreeNode* root){
if(!root) return;
robdfs(root->left);
robdfs(root->right);
int choose = root->val+us[root->left].second+us[root->right].second;
int notchoose = max(us[root->left].second, us[root->left].first)+max(us[root->right].second, us[root->right].first);
us[root] = make_pair(choose, notchoose);
}
int rob(TreeNode* root) {
robdfs(root);
return max(us[root].first, us[root].second);
}
};
打家劫舍 IV
题目描述
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums 表示每间房屋存放的现金金额。形式上,从左起第 i 间房屋中放有 nums[i] 美元。
另给你一个整数 k ,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k 间房屋。
返回小偷的 最小 窃取能力。
思路
典型的最小化最大值问题。
假设题目所求的值为ans,ans满足 m i n ( n u m s [ i ] ) ≤ a n s ≤ m a x ( n u m s [ i ] ) min(nums[i]) \leq ans \leq max(nums[i]) min(nums[i])≤ans≤max(nums[i])
观察可得:题目所求的子序列中窃取能力ans越大,所窃取的房间数目k越大;反之更小。符合二分的单调性要求。
因为求满足房间数k的最大不连续子序列中的最大金额比较难求,所以反过来求满足房间最大金额ans的最大不连续子序列长度。
求满足最大窃取能力为ans,最大不连续序列长度n。为dp问题,dp条件是不能连续并且只能选金额小于等于ans的房间。
d
p
[
0
]
=
1
d
p
[
1
]
=
m
a
x
(
d
p
[
0
]
,
1
]
)
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
,
d
p
[
i
−
2
]
+
1
)
,
i
≥
2
dp[0] = 1 \\ dp[1] = max(dp[0], 1])\\ dp[i] = max(dp[i-1], dp[i-2]+1),i\geq{2}
dp[0]=1dp[1]=max(dp[0],1])dp[i]=max(dp[i−1],dp[i−2]+1),i≥2
每次求出一个n,比较n与k的关系,n>=k,则ans只可能更小;反之更大。这是一个二分的过程。
代码
class Solution {
public:
int getdp(vector<int>& nums, int mx){
vector<int> dp(nums.size()+10, 0);
dp[0] = nums[0]<=mx?1:0;
dp[1] = nums[1]<=mx?max(dp[0], 1):dp[0];
for(int i=2;i<nums.size();i++){
dp[i] = nums[i]<=mx?max(dp[i-1], dp[i-2]+1):dp[i-1];
}
return dp[nums.size()-1];
}
int minCapability(vector<int>& nums, int k) {
if(nums.size()==1)return nums[0];
int l = 0x3f3f3f3f, r = -0x3f3f3f3f;
for(int i=0;i<nums.size();i++){
if(nums[i]<l)l=nums[i];
if(nums[i]>r)r=nums[i];
}
while(l<=r){
int mid = (l+r)>>1;
if(k>getdp(nums, mid)){
l = mid+1;
}else{
r = mid-1;
}
}
return l;
}
};