动态规划
动规虐我千百遍,我待动规如初恋。又遇见了我们的动态规划,而这次的题目也很有意思————打家劫舍:某法外狂徒张三表示自己是专业的小偷,原则性很强,偷了一家就不偷相邻的两家,这样子警察蜀黍永远都抓不到,张三也表示自己也从来没有落网过。我也是没有想到,力扣官方一改严肃的常态,出了一道这般有趣的题目,那么我们就一起来看看。不过在瞅瞅题目前,我们先来回忆一下动态规划:
模板
动态四部曲
1. 确定dp
的状态 : 定义为一维二维还是三维?表示什么含义?
2. 确定状态转移方程
3. 初始化,也就是dp
数组可以取得的元素的值
4. 自底向上的方式计算dp
,并得出最优值(遍历的顺序)
题目
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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. 确定dp
的状态
dp
数组该定义为几维数组呢?很显然,这里一维数组就够了,表示的意义就是,偷到第i
间屋子时所能获得的最大金额.
2. 确定状态转移方程
在确定状态转移方程的时候,我觉得还是以少推多为好,由特殊规律发现普遍规律。
比如这里我们可以先考虑一间、两间、和三间屋子的情况:如果只有一间屋子,那么dp[i]
就是dp[0]
,值就为nums[0]
,因为只能偷这间屋子; 如果一共有两间屋子,那么显然dp[0]
还是第一间屋子的金额,而dp[1]
,也就是要偷两间屋子的情况,但是由于两间屋子相邻,只能偷一家,那么dp[1]
就取两间屋子的最大值,也就是dp[1] = max(nums[0], nums[1])
; 我们继续分析如果是三间屋子呢?我们就需要分情况了,因为我们要确定第二间的屋子要不要偷. 如果偷了,那么第一间和第三间屋子就不能偷,那么此时盗窃的最大金额就为第二间屋子的金额,也就是dp[2] = nums[1]
,但是如果没有偷,那么第一间和第三间屋子就可以盗窃,那么此时最大金额为dp[2] = nums[0] + nums[2]
。综合下来,所能盗窃的最大金额取其中的最大值dp[2] = max(nums[1],nums[0] + nums[2])
,推到这里,差不多就可以得出普遍规律了。
我们根据之前的推导,可以分析发现,在屋子大于两间的时候,我们的情况就不一样了。在两间或者一间的时候,是选择屋子金额的最大值; 但是屋子在三间以上时,我们就可以得出我们的递推公式。当前状态dp[i]
,可以由两种状态推导出来: 当前屋子被偷,前一间屋子不偷,那么就沿用dp[i-1]
,或者是当前屋子不偷,前一间的屋子被偷,沿用前两间屋子的状态dp[i-2]
,加上当前屋子的金额,也就是dp[i - 2] + nums[i]
,那么此时偷到第i
间屋子的最大金额就为 dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
3. 初始化,也就是dp
数组可以取得的元素的值
在这里dp[0]
和dp[1]
都是可以初始化的,dp[0]
是偷第一间屋子的金额,也就是dp[0] = nums[0]
;而两间屋子的盗窃的最大金额就为两间屋子的最大值,也就是dp[1] = max(nums[0], nums[1])
;
4. 自底向上的方式计算dp
,并得出最优值(遍历的顺序)
遍历顺序到底怎么决定呢?那就需要看我们的状态转移方程了。在这里我们的状态转移方程为dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
;也就是说到i
这里的状态需要用到i-1
的状态,那么我们就先需要得出i-1
的状态才能推出后续的状态,那么这里就是正序遍历。
有些二维的动态规划递推公式,就是倒序遍历。比如我们上次讲到的戳气球,就是掺杂了倒序遍历。
具体代码
class Solution {
public:
int rob(vector<int>& nums) {
//先考虑特殊情况 无房可偷 或者 只此一家
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
vector<int> dp(nums.size());//dp数组,就是最大金额
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);//两家选一家,选取最大值
for (int i = 2; i < nums.size(); i++) {
//如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i],最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
//如果不偷第i房间,那么dp[i] = dp[i - 1],即考虑i-1房
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
代码分析
我们按照所推断的,来进行"排版",首先要初始化dp[0]
和dp[1]
,然后正序遍历数组,返回最后的结果dp[size()-1]
,就得出了所能盗窃的最大金额。
总结
我们张三表示很感谢你的努力,以后回回都能盗窃了,世界因你多了一个“法外狂徒”。难道做题就是为了这个吗?当然不是,至少你发现了成为“法外狂徒”的秘籍————动态规划,对动态规划每天一点点的体会,慢慢深入,你就能进入动态规划的世界了。熟能生巧,多学多练
我是小白,我们一起学习,在代码路上不折不扣!