leetcode 经典动态规划DP算法题目(思路、方法、code)
动态规划最重要的在于设计DP数组,找到相应的动态转移方程
文章目录
-
-
- leetcode 经典动态规划DP算法题目(思路、方法、code)
-
- [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/)
- [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/)
- [213. 打家劫舍 II](https://leetcode-cn.com/problems/house-robber-ii/)
- [337. 打家劫舍 III](https://leetcode-cn.com/problems/house-robber-iii/)
- [53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/)
- [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/)
- [120. 三角形最小路径和](https://leetcode-cn.com/problems/triangle/)
- [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)
- [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)
- [174. 地下城游戏](https://leetcode-cn.com/problems/dungeon-game/)
- [32. 最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/)
- [152. 乘积最大子数组](https://leetcode-cn.com/problems/maximum-product-subarray/)
- [62. 不同路径](https://leetcode-cn.com/problems/unique-paths/)
- [63. 不同路径 II](https://leetcode-cn.com/problems/unique-paths-ii/)
-
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
**注意:**给定 n 是一个正整数。
分析:这个问题,可以看青蛙跳台阶(递归与数学归纳),实际上就是斐波那契数列的变体
在上述中,采用了递归或者正向遍历的方法,实际上动态规划就是一种正向遍历的方法。如果采用递归时,由于f(n)=f(n-1)+f(n-2),故直接递归即可,但是仔细思考,如果用该方法,计算f(n-1)时计算了f(n-2),而计算f(n)时又计算了f(n-2),因此存在大量的重复计算。故在此便考虑用一个数组存储对应的函数值,这便是动态规划思想。
令f(n)表示到达n阶的楼顶的方法数,则f(n)=f(n-1)+f(n-2),正向计算即可。
class Solution {
public:
int climbStairs(int n)
{
vector<int> dp(n+1);
dp[0]=1;dp[1]=1;
for(int i=2;i<=n;i++)
dp[i]=dp[i-1]+dp[i-2];
return dp[n];
}
};
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 。
分析:由于相邻的房间不能同时选择,因此在第k个房间时,有两种情况
- 如果选取了第k个房间,说明第k-1个房间一定不能选,那说明这种情况的最大金额应该是前k-2个房间的最大金额加上第k个房间的金额
- 如果没有选取第k个房间,说明这种情况的最大金额实际上就是前k-1个房间的最大金额
- 因此,如果令 d p [ i ] dp[i] dp[i] 表示从0 到 $i $ 房间能盗窃的最高金额,动态转移方程则为 d p [ i ] = m a x ( d p [ i − 2 ] + a [ i ] , d p [ i − 1 ] ) dp[i]=max(dp[i-2]+a[i],dp[i-1]) dp[i]=max(dp[i−2]+a[i],dp[i−1]) ,其中 a [ i ] a[i] a[i]表示第i个房间的金额
根据此,可以遍历,最终结果应该为 d p [ n − 1 ] dp[n-1] dp[n−1]
class Solution {
public:
int rob(vector<int>& nums)
{
int length=nums.size();
if(length==0) return 0;
if(length==1) return nums[0];
vector<int> dp(length+1);
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
for(int i=2;i<length;i++)
{
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[length-1];
}
};
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
分析:这个题与上一个题的区别在于,房屋是环形的,这意味着,第一个房间和最后一个房间不能同时偷窃。这就意味着,**将原来的问题转化为两个子问题:偷窃1 ~ n-1个房间的最大值以及偷窃 2~n 个房间的最大值。最终,取二者的最大值即为解。**因此可以对上一个问题进行两次dp即可。
class Solution {
public:
int rob(vector<int>& nums)
{
int length=nums.size();
if(length==0) return 0;
if(length==1) return nums[0];
vector<int> dp(length+1);
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
for(int i=2;i<length-1;i++) //先对前n-1进行DP
{
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
int result=dp[length-2]; //存储其值
dp[0]=0;dp[1]=nums[1];
for(int i=2;i<length;i++) //对后n-1进行DP
{
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
//取最大值即可
result=max(result,dp[length-1]);
return result;
}
};
337. 打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
分析:每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷,因此父结点如果偷,则两个字节点就不能偷,父结点如果不偷,则子节点可以偷。注意这里说的是可以而不是一定,只需要父结点不偷的情况下两个子节点能够拿出最多的钱即可。因此有了递归的想法。
- 当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的最大钱 + 右孩子能偷到的最大钱
- 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
- 考虑到在python中常用的返回多值的函数,在此处用了pair来存储结果,其中pair的第一个表示当前节点不偷获得的最大钱,pair的第二个表示当前节点偷的话获得的最大钱,根据此,便可以将上述逻辑轻松地表达出来。
class Solution {
public:
int rob(TreeNode* root)
{
pair<int,int> result=rob_helper(root);
return max(result.first,result.second);
}
pair<int,int> rob_helper(TreeNode* root)
{
pair<int,int> root_result;
pair<int,int> root_left;
pair<int,int> root_right;
if(root==NULL) return root_result;
else
{
root_left=rob_helper(root->left);
root_right=rob_helper(root->right);
}
root_result.first=max(root_left.first,root_left.second)+max(root_right.first,root_right.second);
root_result.second=root_left.first+root_right.first+root->val;
return root_result;
}
};
53. 最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
分析:
动态规划法:动态规划的关键,就是设置dp变量,找到动态转移方程
对于该题而言,由于需要找连续子数组,因此设置 d p dp dp变量时,必须考虑如何设置才能够将连续考虑在内。在此用 d p [ i ] dp[i] dp[i] 表示以第 i i i 个元素结尾的数组的最大和,则 d p [ i ] = m a x ( d p [ i − 1 ] + n u m s [ i ] , n u m