LeetCode动态规划集锦
前言
之前刷的题让我感觉很分散,这次就用集中突破的方式,我觉得这种方式,应该比较容易掌握这方面的知识,再不济,这种思想也能让我在对待难题时,有一定的思路。
技巧
答案
第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2].....dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
学过动态规划的可能都经常听到最优子结构,把大的问题拆分成小的问题,说时候,最开始的时候,我是对最优子结构一梦懵逼的。估计你们也听多了,所以这一次,我将换一种形式来讲,不再是各种子问题,各种最优子结构。所以大佬可别喷我再乱讲,因为我说了,这是我自己平时做题的套路。
第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。
由了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。
正文
关于最终的结果为何要取余1000000007?
- 1000000007是一个质数(素数),对质数取余能最大程度避免结果冲突/重复
- int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。
- int64位的最大值为2^63-1,用最大值模1000000007的结果求平方,不会在int64中溢出。
- 所以在大数相乘问题中,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。
- 取模之后能够计算更多的情况
一、青蛙跳台阶
- 题目
- 解题步骤
a. 决定dp[i]代表什么含义:这里代表的是->跳上一个i级台阶总共有多少种跳法。
b. 找出数组元素之间的关系式:dp[n] = dp[n-1]+dp[n-2]
c. 找出初始条件:dp[1] =1,dp[0] = 1; 为了让算式正确,只能这样做
- 代码
code
答案
class Solution {
public:
int jumpFloor(int n) {
//整个的逆向思维就是下台阶
if(n<=1)
return n;
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];//dp[2] = dp[1]+0=1 dp[3] = dp[2]+dp[1]
return dp[n];
}
};
二、机器人走到网格右下角
- 题目
- 解题步骤
a. 定义dp[i][j]的含义:dp[i][j]:当机器人走到(i,j)位置时,一共有dp[i][j]种路径。
b. dp[i][j] = dp[i-1][j]+dp[i][j-1]
c. dp[i][0] = 1;,dp[0][i] = 1; dp[ 都初始化一遍就没啥问题了。
- 代码
code
答案
class Solution {
public:
int uniquePaths(int m, int n) {
int dp[m][n];
for(int i = 0;i<m;i++)
dp[i][0] = 1;
for(int j = 0;j<n;j++)
dp[0][j] = 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];
}
};
- 优化
其实由这个公式dp[i][j] = dp[i-1][j]+dp[i][j-1]这个公式得知,我们每次除了第0行和第0列,其实,我们每次获取一个值,只需要它的左边的值和上面的值。只要有这两个值就可以了。所以,就生出了一个公式:dp[i]=dp[i]+dp[i-1] (第一个dp[i]是新的这一行新的值,第二个dp[i]是上面的那一行在对应这个位置的值(上边的值),dp[i-1]就是它左边的值。所以,这样就可以优化成功了。
code
答案
public static int uniquePaths(int m, int n) {
if (m <= 0 || n <= 0) {
return 0;
}
int[] dp = new int[n]; //
// 初始化
for(int i = 0; i < n; i++){
dp[i] = 1;
}
// 公式:dp[i] = dp[i-1] + dp[i]
for (int i = 1; i < m; i++) {
// 第 i 行第 0 列的初始值
dp[0] = 1;
for (int j = 1; j < n; j++) {
dp[j] = dp[j-1] + dp[j];
}
}
return dp[n-1];
}
三、最小路径和
- 题目
- 解题步骤
a. dp[i][j]:为到(i,j)位置的路径的数字总和。
b. dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示网格中的值
c. dp[0][0] = arr[0][0] ;dp[i][0] = 1;,dp[0][i] = 1;
- 代码
code
答案
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int rows = grid.size();
int cols = grid[0].size();
if(rows<=0||cols<=0)
return 0;
vector<vector<int>> dp(rows,vector<int>(cols));
dp[0][0] = grid[0][0];
for(int i = 1;i<rows;i++)
{
dp[i][0] = dp[i-1][0]+grid[i][0];
}
for(int j = 1;j<cols;j++)
{
dp[0][j] = dp[0][j-1]+grid[0][j];
}
for(int i = 1;i<rows;i++)
{
for(int j =1;j<cols;j++)
{
dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[rows-1][cols-1];
}
};
四、编辑距离
- 题目
- 解题步骤
a. 当字符串 word1 的长度为 i,字符串 word2 的长度为 j 时,将 word1 转化为 word2 所使用的最少操作次数为 dp[i] [j]。
b. dp[i][j] = min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1])
c. dp[0][0] = 0;dp[0][j] = j;dp[i][0] = i;
- 代码
code
答案
class Solution {
public:
int minDistance(string word1, string word2) {
int rows = word1.size();
int cols = word2.size();
if(rows*cols==0)
return rows+cols;
int dp[rows+1][cols+1];
dp[0][0] = 0;
for(int j = 0;j<=cols;j++)
{
dp[0][j] = j;//这个基本上就是插入操作,有多少个字符,插入多少次,所以,直接赋值j就可以
}
for(int i = 0;i<=rows;i++)
{
dp[i][0] = i;//这个是同上面的
}
for(int i = 1;i<=rows;i++)
{
for(int j= 1;j<=cols;j++)
{
if(word1[i-1]==word2[j-1])//若i-1这个位置==j-1这个位置的字符,则dp[i][j]= dp[i-1][j-1]
dp[i][j] = dp[i-1][j-1];
else
{
dp[i][j] = min(min(dp[i-1][j-1],dp[i][j-1]),dp[i-1][j])+1;
}
}
}
return dp[rows][cols];
}
};
- 优化方法
code
答案
class Solution {
public:
int minDistance(string word1, string word2) {
int n1 = word1.length();
int n2 = word2.length();
if(n1*n2==0)
return n1+n2;
int dp[n2+1];
for(int j = 0;j<=n2;j++)//初始化
dp[j] = j;
for(int i = 1;i<=n1;i++)
{
int temp = dp[0];
dp[0] = i;//要记得随时更新dp[0]的这个值
for(int j = 1;j<=n2;j++)
{
int pre = temp;//pre相当于之前的dp[i-1][j-1]
temp = dp[j];//将之前的dp[j]赋值给temp
if(word1[i-1]==word2[j-1])//如果word1的i个字符和word2的第j个字符是相等的,那么就无需任何操作,该dp[i] = dp[i-1]
dp[j] = pre;
else
{
dp[j] = min(min(dp[j-1],pre),dp[j])+1;//否则就在三种操作中取最小的一种进行+1
}
}
}
return dp[n2];
}
};
五、 零钱兑换
- 题目
- 解题步骤
- dp[r] 代表要凑成r块钱,至少要多少个硬币。
- dp[i] = min(dp[i],1+dp[i-coin])
- dp[r] 都初始化为r+1 这也就是他们最多需要的硬币数+1,其他方案一定小于等于该硬币数。所以如果该初始化有被修改,则一定有硬币可以匹配,否则,则没有返回-1.
- 代码
code
答案
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int dp[amount+1];
for(int i = 1;i<=amount;i++)
dp[i] = amount+1;//初始化就赋的比他最多的情况还要多1,如果有被修改,就是修改后的值,如果没有,就是没有适合的硬币可用
dp[0] = 0;
for(int j = 1;j<=amount;j++)
{
for(int coin:coins)
{
if(j-coin<0)
continue;
dp[j] = min(dp[j],1+dp[j-coin]);
}
}
return dp[amount]==amount+1?-1:dp[amount];//若dp[amount]
}
};
六、 最长递增子序列
- **题目
- 解题步骤
- dp[i] :到第i个字符结尾代表的最长上升子序列是多少?
- dp[i] = if(nums[j]<nums[i]) dp[i]= max(dp[i],dp[j]+1)
- 所以0到nums.size()的初始值都赋值为1.
- 代码
解法一
code
答案
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()==0)
return 0;
int dp[nums.size()+1];//dp[i]代表的是到第i个字符结束的最长子序列是多长?
for(int i = 0;i<nums.size();i++)
{
dp[i] = 1;//在每个位置的初始化都为1
for(int j = 0;j<i;j++)//然后从第0个位置到第i-1个位置开始,如果有比当前的i元素要小的,就是属于递增子序列中的一部分。
{
if(nums[j]<nums[i])//开始判断该元素是不是属于该递增子序列中的一部分。
{
dp[i] = max(dp[i],dp[j]+1);//判断dp[i]的值
}
}
}
int maxValue = 0;
for(int i = 0;i<nums.size();i++)
{
maxValue = max(maxValue,dp[i]);
}
return maxValue;
}
};
解法二
code
答案
class Solution {
int lengthOfLIS(vector<int>& nums) {
/**
dp[i]: 所有长度为i+1的递增子序列中, 最小的那个序列尾数.
由定义知dp数组必然是一个递增数组, 可以用 maxL 来表示最长递增子序列的长度.
对数组进行迭代, 依次判断每个数num将其插入dp数组相应的位置:
1. num > dp[maxL], 表示num比所有已知递增序列的尾数都大, 将num添加入dp
数组尾部, 并将最长递增序列长度maxL加1
2. dp[i-1] < num <= dp[i], 只更新相应的dp[i]
**/
int maxL = 0;
int dp[nums.length];
for(int num : nums) {
// 二分法查找, 也可以调用库函数如binary_search
int lo = 0, hi = maxL;
while(lo < hi) {//这是遍历所有的dp呀 将每个新的num安排在合适的位置,虽然,最终的结果有可能是错的,但是个数会是对的
int mid = lo+(hi-lo)/2;
if(dp[mid] < num)//只要有元素小于当前的这个元素,
lo = mid+1;
else
hi = mid;
}
dp[lo] = num;
if(lo == maxL)//这样求出来,最终的顺序会是错误的,但是元素的个数确实正确的。
maxL++;
}
return maxL;
}
}
七、最长公共子序列
- 题目
- 解题步骤
- dp[i][j] :到text1的第i个字符,到text2的第j个字符,这两个字符串最长的公共子序列。
- dp[i][0] = 0 dp[0][j] = 0
code
答案
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1 = text1.size();
int n2 = text2.size();
if(n1*n2==0)
return 0;
int dp[n1+1][n2+1];
for(int i = 0;i<=n1;i++)
{
dp[i][0] = 0;
}
for(int j = 0;j<=n2;j++)
{
dp[0][j] = 0;
}
for(int i = 1;i<=n1;i++)
{
for(int j = 1;j<=n2;j++)
{
if(text1[i-1]==text2[j-1])//判断i-1与j-1就是判断i,j的值,这个一定要切记
{
dp[i][j] = dp[i-1][j-1]+1;
}
else
{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[n1][n2];
}
};
八、高楼扔鸡蛋问题
九. 最长回文字符串
题目
code
答案
class Solution {
public:
string longestPalindrome(string s) {
//使用动态规划的方式
//1.dp[i][j]:代表从第i到第j个元素的字符串是否是回文字符串
//2.dp[i][j]={
// a. s[i]==s[j] j-i<=2
// b. s[i]==s[j]&&dp[i+1][j-1]?
// }
if(s.size()<=1)
return s;
int n = s.size();
int dp[n][n];
for(int i = 0;i<n;i++)
{
dp[i][i] = true;//只有一个元素的话,肯定是=true的
}
int maxLen = 1;
int begin = 0;
for(int L = 2;L<=n;L++)//这个L其实就是控制dp[i][j]有多少个元素的情况
{
for(int i = 0;i<n;i++)
{
int j = L+i-1;
if(j>=n)
break;
if(s[i]!=s[j])
dp[i][j] = false;
else
{
if(j-i<3)
dp[i][j] = true;
else
{
dp[i][j]= dp[i+1][j-1];
}
}
if(dp[i][j]&&(j-i+1)>maxLen)
{
maxLen = j-i+1;
begin = i;
}
}
}
return s.substr(begin,maxLen);
}
};
参考
当你在凝视深渊时,深渊也在凝视你。