首先引入知乎大佬的一篇文章
https://zhuanlan.zhihu.com/p/91582909
这篇文章非常详细,我就是根据这篇文章开始学习动态规划,配合leetcode刷题,希望大家都能战胜dp。
一、青蛙跳台阶
1、经典爬楼梯
https://leetcode-cn.com/problems/climbing-stairs/
这是一道动态规划入门题目,关键在于初始值取值的问题,不能忽略第二阶台阶也是初始值,因此也要初始化。
class Solution {
public:
int climbStairs(int n) {
if(n <= 2)
return n;
vector<int>dp(n+1,0);
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for(int i=3;i<=n;i++)
{
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
https://leetcode-cn.com/problems/three-steps-problem-lcci/
这道题是把两种上台阶方法变成了三种,可以当作爬楼梯类题练习
2、爬楼梯变形
https://leetcode-cn.com/problems/min-cost-climbing-stairs/
这道题困扰了我很久,可能还是太菜了的缘故。首先的困难在dp数组的定义,这道题要求到达楼顶的最小花费,因此我们设dp[i]为到达第i阶楼顶的最小花费,那么状态转移方程就可以写为dp[i]=min(dp[i-1]+cost[i], dp[i-2]+cost[i-1])不太容易理解,也就是说到达第i阶楼顶的最小花费等于到达第i-1阶楼顶(也就是第i阶楼梯)的最小花费加上第i阶的最小花费(相对于cost数组来说的)...的最小值。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int>dp(cost.size()); //dp[i]表示到达i阶台阶顶部需要的最小花费
dp[0] = 0;
dp[1] = min(cost[0],cost[1]);
for(int i=2;i<cost.size();i++)
{
dp[i] = min(dp[i-1] + cost[i], dp[i-2] + cost[i-1]);
}
return dp[cost.size()-1];
}
};
优化的话还是通过三个变量,利用滚动数组的思想进行优化。设为f0,f1,cur,每次更新令f0 = f1,f1 = cur, cur = min;就可以把空间复杂度从O(n)降到O(1)。
二、寻找路径问题
1、机器人寻路问题
https://leetcode-cn.com/problems/unique-paths/
这是一道二维数组的动态规划问题。需要注意的是确定初始值时第一行和第一列所有元素都是初始值,因为位于第一行和第一列的元素只能向右或者向下走,只有一条路径。其他代码与一维数组大同小异。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
for(int i=1;i<=m;i++)
{
dp[i][1] = 1;
}
for(int i=1;i<=n;i++)
{
dp[1][i] = 1;
}
for(int i=2;i<=m;i++)
{
for(int j=2;j<=n;j++)
{
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m][n];
}
};
2、最小路径和
https://leetcode-cn.com/problems/minimum-path-sum/
这和上面那道机器人寻路问题解法根源上是一样的,只不过在dp数组的意义及dp数组值得出的方式上有些变化。还是使用二维数组存储每个点的最小路径和,每个点的最小路径和等于它左边节点和上面节点最小路径和的最小值再加上该节点本身的值。需要注意的是初始化的时候要初始化第一列和第一行。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
vector<vector<int>>dp = grid;
for(int i=1;i<grid.size();i++)
{
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for(int j=1;j<grid[0].size();j++)
{
dp[0][j] = dp[0][j-1] + grid[0][j];
}
for(int i=1;i<grid.size();i++)
{
for(int j=1;j<grid[0].size();j++)
{
dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[grid.size()-1][grid[0].size()-1];
}
};
三、编辑距离
https://leetcode-cn.com/problems/edit-distance/
这道题属于hard难度的题,他的难点首先在于dp数组不好定义,其次在于状态转移方程不好理解。针对这道题我们用二维数组解决问题,定义dp[i][j]为word1前i个字符变成word2前j个字符需要经过的最小操作数。根据题目可以知道有三种转移方式:一是删除操作,也就是从word1的前i-1个子串变成word2的前j子串需要dp[i-1][j]次操作,那么dp[i][j]=dp[i-1][j]+1也就是对word1的第i个字符进行删除操作;二是插入操作,也即从word1的前i个子串变成word2的前j-1个子串需要dp[i][j-1]次操作,那么dp[i][j]=dp[i][j-1]也就是在word1后插入一个字符就得到了word2;三是替换操作,也即word1的前i-1个子串变成word2的j-1个 子串所需要的操作。注意这里如果word1的第i个字符等于word2的第j个字符,那么dp[i][j]=dp[i-1][j-1]也就是说不进行替换操作。
所以最后的状态转移方程为dp[i][j]=min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+(word1[i-1] == word2[j-1]))此题得解。
class Solution {
public:
int minDistance(string word1, string word2) {
int n1 = word1.length(), n2 = word2.length();
if(n1 * n2 == 0)
return n1 + n2;
vector<vector<int>>dp(n1+1,vector<int>(n2+1,0));
for(int i=0;i<=n1;i++)
{
dp[i][0] = i;
}
for(int i=0;i<=n2;i++)
{
dp[0][i] = i;
}
for(int i=1;i<=n1;i++)
{
for(int j=1;j<=n2;j++)
{
if(word1[i-1] == word2[j-1])
{
dp[i][j] = 1 + min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1]-1);
}
else
dp[i][j] = 1 + min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1]);
}
}
return dp[n1][n2];
}
};
四、最长递增序列
1、最长连续递增序列
https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/
循序渐进,先来一道比较简单的递增序列问题。这道题dp数组定义显而易见,即dp[i]是以nums[i]结尾最长连续子序列的长度,问题迎刃而解。空间优化的话就是用一个int变量存储当前值,用一个存储最大值即可。
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if(nums.empty())
return 0;
//vector<int>dp(nums.size(),1);
int res = 1, first = 1;
for(int i=1;i<nums.size();i++)
{
if(nums[i] > nums[i-1])
{
first += 1;
res = max(res,first);
}
else
first = 1;
}
return res;
}
};
五、背包问题
1、0-1背包问题
这种题我还没在leetcode上见到。背包问题的描述一般是,有一些物品的重量由weight数组给出,价值由value数组给出,问当背包重量为w时能装最大价值为多少?
首先dp数组dp[i][j]定义为前i个物品的总重量,所以我们只需要dp取最大就行。那么状态转移是怎么样呢?面对第i个物品有两种选择,第一种是不把它放入背包,这时dp[i][j]和dp[i-1][j]是相等的;第二种是放入背包,那么dp[i][j]就等于前一个状态的最大价值和dp[i][j-value[i-1]]取最大值。再注意一下边界值的问题就可以解决了。
2、完全背包问题
这道题在leetcode上是以零钱兑换的形式出现的。
https://leetcode-cn.com/problems/coin-change-2/
完全背包和0-1背包的区别在于物品数目是否无限。状态转移方程和之前的0-1背包差不多,区别在于一个求最大值,一个求和
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<vector<int>>dp(coins.size()+1,vector<int>(amount+1));
for(int i=0;i<=amount;i++)
{
dp[0][i] = 0;
}
for(int i=0;i<=coins.size();i++)
{
dp[i][0] = 1;
}
for(int i=1;i<=coins.size();i++)
{
for(int j=1;j<=amount;j++)
{
if(j >= coins[i-1])
dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]];
else
dp[i][j] = dp[i-1][j];
}
}
return dp[coins.size()][amount];
}
};