前言
大家好,我是「程序员小熊」,就职于「华为」。今天给大家带来一道与「动态规划」相关的题目,这道题同时也是包括字节、微软和谷歌等互联网大厂的面试题,即力扣上的第 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 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
解题思路
相信了解「动态规划」的童鞋,只要看到「求最优解」的问题,立刻会想到可以通过「动态规划」去解答。没错,本题也可以通过动态规划去做。
「举例1」
假如只有一间房子,这间房子里藏有 100 元现金,即 nums = [100]。
由于只有一间房子,所以无需考虑「系统会自动报警」,所以这种情况「专业」小偷能够偷盗的最高金额为 100 元。
「举例2」
假如有两间房子,这两间房子里分别藏有 300、500 和 700 元现金,即 nums = [300,500,700]。
由于小偷是「专业」的,所以会考虑「系统会自动报警」的情况,也会考虑如何偷盗才能获取最高金额。
当前主要有「两种策略」:
1、偷盗第三间房子,则第二间房子不能偷盗(「但第一间房子可以偷盗」),此时获取的金额为:300(偷盗第一间房子,获取的现金) + 700(偷盗第三间房子,获取的现金)= 1000;
2、不偷盗第三间房子,则前面两间房子可以任选一间偷盗,此时获取的金额为:500(偷盗第二间房子,获取的现金),如果偷盗第一间房子,只能获取更少的 300 元;
由于小偷足够「专业」,因此 ta 一定会选择策略 1,获取 1000 元现金。
一维dp
根据上面的分析,可以得出:
「定义状态」
dp[i]:盗窃到 i 间房屋所能盗窃到的最高总金额。
「状态转移方程」
dp[i] = max(dp[i−2] + nums[i], dp[i−1]) (n ≥ 2)
「边界条件」
dp[0]=nums[0],房子从第1间开始算;
dp[1]=max(nums[0], nums[1])。
「举例」
以数组 nums = [2,7,9,3,1] 为例子,如下图示。
遍历房子,求盗取最大金额。
完整过程,如下动图示。
Show me the Code
「C++」
int rob(vector<int>& nums) {
int houseNum = nums.size();
/* 只有一间房子 */
if (houseNum == 1) {
return nums[0];
}
vector<int> dp = vector<int>(houseNum, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < houseNum; i++) {
/* 有不少于两间房子,取最后一间房子选跟不选所能偷盗的最大金额 */
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[houseNum - 1];
}
「复杂度分析」
时间复杂度:「O(n)」,其中 n 是数组的长度,需要遍历一遍数组。
空间复杂度:「O(n)」,其中 n 是数组的长度,开辟额外的长度为 n 的数组。
「进一步优化」
由状态转移方程可知,dp[i] 只与 dp[i-2]、dp[i-1] 和 nums[i] 有关,所有没有必要去定义一个 dp 数组,直接定义两个变量 prevMax 和 currMax 即可,其中:
prevMax:记录偷盗第 i 间房子之前,盗取到的最大金额。
currMax:记录偷盗第 i 间房子(包括第 i 个),盗取到的最大金额。
Show me the Code
「C」
int rob(int* nums, int numsSize){
int prevMax = 0, currMax = 0;
for (int i = 0; i < numsSize; i++) {
int temp = currMax;
currMax = fmax(prevMax + nums[i], currMax);
prevMax = temp;
}
return currMax;
}
「C++」
int rob(vector<int>& nums) {
int prevMax = 0, currMax = 0;
for (int i = 0; i < nums.size(); i++) {
int temp = currMax;
currMax = max(prevMax + nums[i], currMax);
prevMax = temp;
}
return currMax;
}
「Java」
int rob(int[] nums) {
int prevMax = 0, currMax = 0;
for (int i = 0; i < nums.length; i++) {
int temp = currMax;
currMax = Math.max(prevMax + nums[i], currMax);
prevMax = temp;
}
return currMax;
}
「Python3」
def rob(self, nums: List[int]) -> int:
prevMax = currMax = 0
for i in range(len(nums)):
temp = currMax
currMax, prevMax = max(prevMax + nums[i], currMax), temp
return currMax
「Golang」
func rob(nums []int) int {
prevMax, currMax := 0, 0
for i := 0; i < len(nums); i++ {
temp := currMax
currMax, prevMax = max(prevMax + nums[i], currMax), temp
}
return currMax
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
「复杂度分析」
时间复杂度:「O(n)」,其中 n 是数组的长度,需要遍历一遍数组。
空间复杂度:「O(1)」,未开辟额外的存储空间。
二维dp
从上面的分析可知,「专业的」小偷在考虑如何偷盗最高金额时,通常有两种策略,即某个房子「盗窃还是不盗窃」,因为可以通过「二维dp」来做。
dp[i][0]:当前房子不盗窃所获金额。
dp[i][1]:当前房子盗窃所获金额。
因此此时的状态转移方程为:
dp[i][1] = num[i - 1] + dp[i - 1][0];
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]);
maxProfit = max(dp[houseNum][0], dp[houseNum][1])
也就是:
当前房子盗窃所获取到的金额为:当前房子的金额加上其前一房子不盗窃所获得金额的总和;
当前房子不盗窃所获取到的金额为:取其前一个房子不盗窃和盗窃所获取金额的最大值;
最终的最大金额取当前最后一个房子不盗窃和盗窃所获金额的最大值。
同样,可以直接定义两个变量 prevNo 和 prevYes 去求解即可,没必要定义一个二维 dp 数组。
Show me the Code
「C++」
int rob(vector<int>& nums) {
int prevNo = 0;
int prevYes = 0;
for (int n : nums) {
int temp = prevNo;
prevNo = max(prevNo, prevYes);
prevYes = n + temp;
}
return max(prevNo, prevYes);
}
「Python3」
def rob(self, nums: List[int]) -> int:
prevNo = prevYes = 0
for i in range(len(nums)):
temp = prevNo
prevNo, prevYes = max(prevNo, prevYes), nums[i] + temp
return max(prevNo, prevYes)
「Golang」
func rob(nums []int) int {
prevNo, prevYes := 0, 0
for i := 0; i < len(nums); i++ {
temp := prevNo
prevNo, prevYes = max(prevNo, prevYes), nums[i] + temp
}
return max(prevNo, prevYes)
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
「复杂度分析」
时间复杂度:「O(n)」,其中 n 是数组的长度,需要遍历一遍数组。
空间复杂度:「O(1)」,未开辟额外的存储空间。
往期精彩回顾
更多精彩
关注公众号「程序员小熊」