《代码随想录》刷题打卡day34:动态规划-part07

发布时间:2026/8/30 5:34:46
《代码随想录》刷题打卡day34:动态规划-part07 文章目录【198.打家劫舍】【213.打家劫舍II】【337.打家劫舍III】总结【198.打家劫舍】思路当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。所以这里就更感觉到当前状态和前面状态会有一种依赖关系那么这种依赖关系都是动规的递推公式。剩余按照五部曲来想确定dp数组dp table以及下标的含义dp[i]考虑下标i包括i以内的房屋最多可以偷窃的金额为dp[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房注意这里是考虑并不是一定要偷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]);代码如下vectorintdp(nums.size());dp[0]nums[0];dp[1]max(nums[0],nums[1]);确定遍历顺序dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的那么一定是从前到后遍历代码如下for(inti2;inums.size();i){dp[i]max(dp[i-2]nums[i],dp[i-1]);}举例推导dp数组classSolution{public:introb(vectorintnums){vectorintdp(nums.size(),0);// dp[i]表示考虑下标i包括i的房屋可偷窃到的最高金额dp[0]nums[0];if(nums.size()1)returnnums[0];dp[1]max(nums[0],nums[1]);for(inti2;inums.size();i){dp[i]max(dp[i-2]nums[i],dp[i-1]);}returndp[nums.size()-1];}};【213.打家劫舍II】思路这道题目和198.打家劫舍是差不多的唯一区别就是成环了。对于一个数组成环的话主要有如下三种情况情况一考虑不包含首尾元素情况二考虑包含首元素不包含尾元素情况三考虑包含尾元素不包含首元素注意这里用的是考虑例如情况三虽然是考虑包含尾元素但不一定要选尾部元素 对于情况三取nums[1] 和 nums[3]就是最大的。而情况二 和 情况三 都包含了情况一了所以只考虑情况二和情况三就可以了。classSolution{public:introb(vectorintnums){if(nums.size()0)return0;if(nums.size()1)returnnums[0];intresult1robRange(nums,0,nums.size()-2);intresult2robRange(nums,1,nums.size()-1);returnmax(result1,result2);}introbRange(vectorintnums,intstart,intend){if(endstart)returnnums[start];vectorintdp(nums.size());dp[start]nums[start];dp[start1]max(nums[start],nums[start1]);for(intistart2;iend;i){dp[i]max(dp[i-2]nums[i],dp[i-1]);}returndp[end];}};【337.打家劫舍III】思路对于树的话首先就要想到遍历方式前中后序深度优先搜索还是层序遍历广度优先搜索。本题一定是要后序遍历因为通过递归函数的返回值来做下一步计算。与198.打家劫舍213.打家劫舍II一样关键是要讨论当前节点抢还是不抢。如果抢了当前节点两个孩子就不能动如果没抢当前节点就可以考虑抢左右孩子注意这里说的是“考虑”动态规划其实就是使用状态转移容器来记录状态的变化这里可以使用一个长度为2的数组记录当前节点偷与不偷所得到的的最大金钱。这道题目算是树形dp的入门题目因为是在树上进行状态转移我们在讲解二叉树的时候说过递归三部曲那么下面我以递归三部曲为框架其中融合动规五部曲的内容来进行讲解。确定递归函数的参数和返回值这里我们要求一个节点 偷与不偷的两个状态所得到的金钱那么返回值就是一个长度为2的数组。参数为当前节点代码如下vectorintrobTree(TreeNode*cur){其实这里的返回数组就是dp数组。所以dp数组dp table以及下标的含义下标为0记录不偷该节点所得到的的最大金钱下标为1记录偷该节点所得到的的最大金钱。所以本题dp数组就是一个长度为2的数组那么有同学可能疑惑长度为2的数组怎么标记树中每个节点的状态呢别忘了在递归的过程中系统栈会保存每一层递归的参数。如果还不理解的话就接着往下看看到代码就理解了哈。确定终止条件在遍历的过程中如果遇到空节点的话很明显无论偷还是不偷都是0所以就返回if (cur NULL) return vectorint{0, 0};这也相当于dp数组的初始化确定遍历顺序首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。通过递归左节点得到左节点偷与不偷的金钱。通过递归右节点得到右节点偷与不偷的金钱。代码如下// 下标0不偷下标1偷vectorintleftrobTree(cur-left);// 左vectorintrightrobTree(cur-right);// 右// 中确定单层递归的逻辑如果是偷当前节点那么左右孩子就不能偷val1 cur-val left[0] right[0]; 如果对下标含义不理解就再回顾一下dp数组的含义如果不偷当前节点那么左右孩子就可以偷至于到底偷不偷一定是选一个最大的所以val2 max(left[0], left[1]) max(right[0], right[1]);最后当前节点的状态就是{val2, val1}; 即{不偷当前节点得到的最大金钱偷当前节点得到的最大金钱}代码如下vectorintleftrobTree(cur-left);// 左vectorintrightrobTree(cur-right);// 右// 偷curintval1cur-valleft[0]right[0];// 不偷curintval2max(left[0],left[1])max(right[0],right[1]);return{val2,val1};举例推导dp数组解法/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */classSolution{public:introb(TreeNode*root){vectorintresultrobTree(root);returnmax(result[0],result[1]);}// 长度为2的数组下标0不偷下标1:偷vectorintrobTree(TreeNode*cur){if(curNULL)returnvectorint{0,0};vectorintleftrobTree(cur-left);vectorintrightrobTree(cur-right);// 偷cur则不能偷左右孩子intval1cur-valleft[0]right[0];// 不偷cur则可选择偷左右孩子但不是必须要选较大的情况intval2max(left[0],left[1])max(right[0],right[1]);return{val2,val1};}};总结这道题是树形DP的入门题目通过这道题目大家应该也了解了所谓树形DP就是在树上进行递归公式的推导。所以树形DP也没有那么神秘只不过平时习惯了在一维数组或者二维数组上推导公式一下子换成了树就需要对树的遍历方式足够了解