Leetcode练习题:动态规划
在进行这部分学习之前,我根本没有正式的学习过动态规划,遇到动态规划的题更是完全做不出来,即使我看别人解题的代码,也很难理解dp是个什么意思。但是学习了之后,了解了动态规划,感觉还是有难度,解题的关键就在于怎么来设定dp记录的东西才有意义能解决问题。
1.确认原问题与子问题
2.确认状态
2.确认边界状态的值
4.确认状态转移方程
62:不同路径
问题描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
向右 -> 向右 -> 向下
向右 -> 向下 -> 向右
向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
解题思路
这题比较简单,建立一个二维dp数组,该数组记录的是达到该点有几种路径,边界值是第一行和第一列,只有一种走法,之后的每一格达到路径=左边格到达路径+上边格到达路径。最后返回右下角的值
代码实现
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n));
dp[0][0]=0;
for(int j=1;j<n;j++)
{
dp[0][j]=1;
}
for(int i=1;i<m;i++)
{
dp[i][0]=1;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
反思与收获
注意边界状态的值
63:不同路径Ⅱ
问题描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
说明:m 和 n 的值均不超过 100。
示例 1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
-
向右 -> 向右 -> 向下 -> 向下
-
向下 -> 向下 -> 向右 -> 向右
解题思路
这题多了障碍物,如果该点就是障碍物的话,dp直接为0.
需要特别考虑一下的是,如果障碍物在第一行或者是第一列,则只会的格子将无法达到,直接为0.
代码实现
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid)
{
int m=obstacleGrid.size();
int n=obstacleGrid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
dp[0][0]=0;
//第一行,如果有障碍物 后面都无法达到
for(int j=1;j<n;j++)
{
if(obstacleGrid[0][j]==1)
{
break;
}
dp[0][j]=1;
}
//第一列同理
for(int i=1;i<m;i++)
{
if(obstacleGrid[i][0]==1)
{
break;
}
dp[i][0]=1;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
dp[i][j]=obstacleGrid[i][j]==1?0:dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
反思与收获
动态规划特别要考虑起始值,以及特殊情况。
72:编辑距离
问题描述
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
解题思路
这题难度较高,不知道dp该怎么建了,我看了题解建的dp,也是理解了很久很久。
建立一个二维dp[i][j]表示将word1前i个的字符串变成word2前j个字符串最少需要几步。
^ ^ r o s
^ 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3
假设word1为空,则需要不断增加,于是第一行是0 1 2 3,假设word2为空,则需要不断删除,于是第一列是0 1 2 3 4 5
变化有三种情况
1.增加,那等于左边步骤+1 h->ros
2.删除,那等于上边步骤+1 ho->r
3.修改,直接变,那等于左上角步骤+1,hor->ros
但如果该位置字符相同,比如ho->ro的o时则不变,注意下标,比较的是在word1下标为i-1的字符和word2中下标为j-1的字符
代码实现
int minDistance(string word1, string word2) {
int len1=word1.size()+1;
int len2=word2.size()+1;
vector<vector<int>> dp(len1,vector<int>(len2,0));
for(int i=0;i<len1;i++)
{
dp[i][0]=i;
}
for(int j=0;j<len2;j++)
{
dp[0][j]=j;
}
for(int i=1;i<len1;i++)
{
for(int j=1;j<len2;j++)
{
dp[i][j]=min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
if(word1[i-1]==word2[j-1])
{
dp[i][j]=min(dp[i][j],dp[i-1][j-1]);
}
}
}
// for(int i=0;i<len1;i++)
// {
// for(int j=0;j<len2;j++)
// {
// cout<<dp[i][j]<<" ";
// }
// cout<<endl;
// }
return dp[len1-1][len2-1];
}
反思与收获
想不出来dp怎么建,就根本做不来,现在学到了,将两个字符串进行编辑变化,可以看成而矩阵,将字符串分别看上列和行,比较一部分相同时需要几步,dp建出来,状态变化这题还是好分析。
139:单词拆分
问题描述
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
解题思路
dp[i]记录该位置之前的字符串是否能被拆分,
状态转换:判断当前位置能否拆分就变成了
遍历该字符串所有的位置,如果该位置可以被拆分,并且剩下的部分存在在字典中,那当前位置可拆分。
dp[0]=true
代码实现
bool wordBreak(string s, vector<string>& wordDict) {
if(s.empty()||wordDict.empty())
{
return false;
}
//存放字典 可以使用find
set<string> m;
for(int i=0;i<wordDict.size();i++)
{
if(wordDict[i]!="")
{
m.insert(wordDict[i]);
}
}
vector<bool> dp(s.length()+1,false);
//初始值
dp[0]=true;
for(int i=1;i<=s.length();i++)
{
for(int j=0;j<i;j++)
{
//状态转换方程
if(dp[j]&&m.find(s.substr(j,i-j))!=m.end())
{
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
反思与收获
这里的dp数组大小是s.length()+1,因为是记录该位置之前的字符串是否符合要求
|0 |1 |2 | 理解成为竖线的位置。判断字符串单词拆分也可以使用动态规划的问题,因为该字符串能否被拆分,可以分解成小问题,前面部分能否被拆分,以及剩下的部分是不是在字典中。
221:最大正方形
问题描述
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4
解题思路
首先建立一个二维dp数组,那dp[i][j[表示什么呢,如何来表示正方形。
通常我们使用左上角和右下角来表示一个矩阵,而正方形可以只利用右下角一个坐标来表示,存的肯定是正方形的边长。
转换方程:这个点能否扩大正方形,或者说它决定的正方形大小,实质上取决于,它上面点和它左面点以及它左上角的点决定的。
1 0 | 1 1|1 1
0 1 | 0 1| 1 1
边界值是第一行与第一列
代码实现
int maximalSquare(vector<vector<char>>& matrix) {
//dp[i][j]表示? 以其为右下角的正方形边长,
if(matrix.size()==0||matrix[0].size()==0)
{
return 0;
}
int m=matrix.size();
int n=matrix[0].size();
vector<vector<int>> dp(m,vector<int>(n));
int ans=0;
for(int i=0;i<m;i++)
{
dp[i][0]=matrix[i][0]=='1'? 1:0;
ans=max(ans,dp[i][0]);
}
for(int j=1;j<n;j++)
{
dp[0][j]=matrix[0][j]=='1'? 1:0;
ans=max(ans,dp[0][j]);
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(matrix[i][j]=='1')
{
dp[i][j]=min(min(dp[i][j-1],dp[i-1][j-1]),dp[i-1][j])+1;
}
ans=max(ans,dp[i][j]);
}
}
return ans*ans;
}
反思与收获
理解成正方形不断扩大,画图会比较好理解,这里的转换方程稍微复杂一点。
309:最佳买卖股票时机含冷冻期
问题描述
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
解题思路
这题dp太难设了,参考了官方题解。
首先分析有n天,每天结束后,注意是结束后,这样才能理解只有三种状态
1.有股票,可能是一直持有的,但是也可能是今天买的,所以要注意今天交易结束后
2.1没有股票,在冷冻期,所以是今天卖出去的
2.2没有股票,不是冷冻期,所以今天啥也没干
因此转换方程为
有股票一直没卖的情况,一直持有或者前一天无股票并且不是冷冻期-今天买了股票花费的钱
dp[i][0]=max(dp[i-1][0],dp[i-1][2]-price[i])
没有股票冷冻期,今天卖了,那前一天一定是持有股票的状态
dp[i][1]=dp[i-1][0]+price[i]
没有股票不是冷冻期,啥也没干,那就是前一天延续下来的无股票情况
dp[i][2]=max(dp[i-1][1],dp[i-1][2])
注意初始化
代码实现
int maxProfit(vector<int>& prices) {
int n=prices.size();
vector<vector<int>> dp(n,vector<int>(3));
/*三种情况 都是当天结束后:
1.有股票,则可能是一直拿着的,也可能是今天买的
2.没有股票,在冷冻期,注意是当天结束后,所以是今天卖出去的
3.没有股票,也不在冷冻期,那今天什么事都没有做。*/
dp[0][0]=-prices[0];
dp[0][1]=0;
dp[0][2]=0;
for(int i=1;i<n;i++)
{
//一直拿着不卖的,或者是直接前一天没有,今天买入的
dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]);
//冷冻 因为今天是今天卖了
dp[i][1]=dp[i-1][0]+prices[i];
//无股票也不冷冻,啥也没做
dp[i][2]=max(dp[i-1][1],dp[i-1][2]);
}
return max(dp[n-1][0],dp[n-1][1])==dp[n-1][0]?max(dp[n-1][0],dp[n-1][2]):max(dp[n-1][1],dp[n-1][2]);
}
反思与收获
这种题目,情况分析出来想明白了,代码也就敲出来了。但是如果题目没看明白,或者dp没有想要怎么建,那就是一行都敲不出来,就看想不想的明白,有时看别人题解也看不清楚。
这题挺难的,一天还有三种情况的,而且考虑的是交易完成后,不然的还要再难想一点。遇到这样的问题,先分析状态,把所有情况都分析出来,以及之间是怎么转换的,这一天只能是从前一天的状态变化过来的。
312:戳气球
问题描述
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 300, 0 ≤ nums[i] ≤ 100
示例:
输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167
解题思路
看了官方题解才会的。
设置一个dp[i][j],表示开区间(i,j)中能获取的最大值,
显然设置一个循环,设戳破的为k,则dp[i][j]应为最大的:dp[i][k]+dp[k][j]+nums[k]*nums[i]*nums[j]
为什么是乘以nums[i]*nums[j]因为是把中间的都戳破的,k是最后一个戳破的,于是k与i,j是相邻的
从小区间一点点扩展到大区间,注意i,j循环的起始条件和变化。
首先要首尾插入1
代码实现
int maxCoins(vector<int>& nums) {
int n=nums.size();
vector<vector<int>> dp(n+2,vector<int>(n+2));
nums.insert(nums.begin(),1);
nums.push_back(1);
//dp[i][j] (i,j)区间最大
/*你就从 (i,j) 开区间只有三个数字的时候开始计算,储存每个小区间可以得到金币的最大值
然后慢慢扩展到更大的区间,利用小区间里已经算好的数字来算更大的区间*/
for(int i=n-1;i>=0;i--)
{
for(int j=i+2;j<=n+1;j++)
{
//k是最后一个被戳破的
for(int k=i+1;k<j;k++)
{
int sum=nums[k]*nums[i]*nums[j];
sum+=dp[i][k]+dp[k][j];
dp[i][j]=max(dp[i][j],sum);
}
}
}
return dp[0][n+1];
}
反思与收获
这题可能能想到用dp[i][j]表示区间i,j最大的情况,但是注意表示的开区间,但是情况分析不出来,特别是注意k是最后戳破的,这样才能跟ij相邻,才能进行计算,因此需要从小区间拓展到大区间。其实也是写了一个函数,判断i,j之间的所有位置k,选择最优的情况。
343:整数拆分
问题描述
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
解题思路
dp[i]表示将i拆分成整数能分解的最大乘积,则
子问题分解:10可以看成是3*dp[7],要拆分的7
因此对于一个数,可以使用一个循环,找到每一种拆分的情况,取最大值为答案,最后返回dp[n],0和1不能拆分,因此dp[0]=dp[1]=0
代码实现
int integerBreak(int n) {
vector<int> dp(n+1);
dp[0]=dp[1]=0;
for(int i=2;i<=n;i++)
{
int max_cur=0;
for(int j=1;j<i;j++)
{
max_cur=max(max_cur,max(j*(i-j),j*dp[i-j]));
}
dp[i]=max_cur;
}
return dp[n];
}
反思与收获
常常使用一个循环,来尝试所有的子问题分解的情况,取最优的情况更新为该状态的dp值。
413:等差数列拆分
问题描述
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,以下数列为等差数列:
1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9
以下数列不是等差数列。
1, 1, 2, 5, 7
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。
如果满足以下条件,则称子数组(P, Q)为等差数组:
元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。
函数要返回数组 A 中所有为等差数组的子数组个数。
示例:
A = [1, 2, 3, 4]
返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。
解题思路
判断这个数列是否为等差数列可以拆分成小问题:前部分数列是否为等差数列,且前数列最后两个的差值是否等于最后元素与前数列最后元素的差值。
用dp[i]表示,以i为结尾的等差数列数量。
dp[0]=dp[1]=0
如果A[i]能够添加到等差数列后面,则dp[i]=dp[i-1]+1
用ans将以每个位置结束的等差数列个数相加记录,则为答案
代码实现
int numberOfArithmeticSlices(vector<int>& A) {
vector<int> dp(A.size());
int ans=0;
//dp[i]是以i为结尾的 等差数列 个数
for(int i=2;i<A.size();i++)
{
if(A[i]-A[i-1]==A[i-1]-A[i-2])
{
dp[i]=1+dp[i-1];
ans+=dp[i];
}
}
return ans;
}
反思与收获
一维数组动态问题当中,dp[i]总是来记录以i为结尾的子序列是否符合要求,有时也会用到dp[i][j]来表示下标在i到j的子序列。
746:使用最小花费爬楼梯
问题描述
数组的每个索引作为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 costi。
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例 1:
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
示例 2:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
解题思路
该题跟最经典基础的动态走楼梯问题类似,可以走一步或者两步。
但我一开始,还是会搞混有点想不明白,注意这个cost是踩上这个阶梯花费的经历
因此我们用dp[i]表示踏上这个阶梯所耗费的精力
走了一步或者是走了两步+踏上该阶梯花费的力气cost[i]
dp[i]=min(dp[i-1],dp[i-2])+cost[i]
到楼梯顶端,则为min(dp[i-1],dp[i-2])。
代码实现
int minCostClimbingStairs(vector<int>& cost) {
int s=cost.size();
vector<int> dp(s);
dp[0]=cost[0];
dp[1]=cost[1];
for(int i=2;i<s;i++)
{
dp[i]=min(dp[i-1],dp[i-2])+cost[i];
}
return min(dp[s-1],dp[s-2]);
}
反思与收获
一定要分析清楚状态,有几种走法还是花费多少力气,甚至是踩上了这个阶梯都要分清状态时怎么样的,比如之前股票的题目,也是要分清楚。
——————————————————————————————————
动态规划的题大多都挺有意思…因此难度也比较大,大就大在它基本不能用暴力解决或者是其他办法,只能使用动态规划,时间才满足要求,但是动态规划就是这样,想不出来dp怎么建立,那这道题就做不出来了,只要告诉你dp怎么建也就相当于告诉你代码是什么了。
常常感觉都不是编程题,而是智商题…可能只能靠多做多积累了,甚至题目稍变一下就不会。(;′⌒`)