目录
- 前言
- 正文
- 技巧
- 关于最终的结果为何要取余1000000007?
- 1. 最长回文子串
- 2. [最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/)——动态规划的内容
- 3. [LCS最大子序列问题(最长公共子序列)](https://leetcode-cn.com/problems/longest-common-subsequence/)
- 4. [连续子数组的最大和](https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/)
- 5. [回文字符的数目](https://leetcode-cn.com/problems/palindromic-substrings/)
- 6. 青蛙跳台阶
- 7. 机器人走到网格右下角
- 8. 最小路径和
- 9. 编辑距离
- 10. 零钱兑换
- 11. 最长递增子序列
- 12. 最长公共子序列
- 13. 高楼扔鸡蛋问题
- 14. 最长回文字符串
- 参考
前言
正文
技巧
第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 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里面不会溢出。
- 取模之后能够计算更多的情况
1. 最长回文子串
code
class Solution {
public:
string longestPalindrome(string s) {
//1. dp[i][j]:从i到j的这个字符串是否是回文串
//2. 找出状态转移方程:
//{
// if s[i]==s[j]
// if(j-i<3)
// dp[i][j] = true;
// else
// dp[i][j] = dp[i+1][j-1]
// else
// dp[i][j]=false;
// 3. 初始化:dp[i][i]=true;
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;//只有一个元素的情况,是不是每个元素都是回文串
}
int maxLen = 1;
int begin = 0;
for(int L = 2;L<=n;L++)//L代表的是子串的长度
{
for(int i = 0;i<n;i++)
{
int j = i+L-1;//j=1 dp[i][j]就是有两个元素
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);
}
};
2. 最长有效括号——动态规划的内容
class Solution {
public:
int longestValidParentheses(string s) {
stack<int> st;//申请一个栈空间
vector<int> mark(s.length());//弄一个标记数组,若该括号是有效的,标记还是0,若不是,则更改为1
for(int i = 0;i<mark.size();i++)
{
mark[i]=0;
}
for(int j=0;j<s.length();j++)
{
if(s[j]=='(')//当左括号是,进栈
{
st.push(j);
}
else//这肯定是右括号了
{
if(st.empty())//这就是无用的右括号了,这个时候只是把右括号的那个位置标记成0
{
mark[j]=1;
}
else
{
st.pop();//若是真的可以匹配的右括号就栈元素出来
}
}
}
while(!st.empty())//,这个点很重要,容易忽视,有可能剩下一些左括号,也要标记成0
{
mark[st.top()]=1;
st.pop();
}
int len = 0;
int ans = 0;
int max1 = 0;
for(int k =0;k<s.length();k++)//从头到尾遍历一遍,看连续的0有多少个
{
if(mark[k])
{
len=0;
continue;
}
len++;
max1 = len;
ans=max(ans,max1);
}
return ans;
}
};
题解
先将所有元素都标成0,将不能匹配的右括号全部置为1.遍历一遍,结束后,将不能匹配的左括号也都置为1.最好判断一下连续最长的0是多少个就可以了。
3. LCS最大子序列问题(最长公共子序列)
code
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
//三步法
//1. 确定dp[i][j]的含义:s[0]-s[i] 与s[0]-s[j]的公共子序列的长度
//2. 动态转移方程:dp[i][j] =
//3. 初始化做好
int n1 = text1.size();
int n2 = text2.size();
if(n1*n2==0)//error0:居然写的太快,把这个==0都漏掉了
return 0;
int dp[n1+1][n2+1];//error1:由于要表示空船的新情况,所以使用n+1的数组
for(int i = 0;i<=n1;i++)
{
dp[i][0] = 0;//空串与另一个子串的公共子序列都为0
}
for(int j = 0;j<=n2;j++)
{
dp[0][j] = 0;//空串与另一个子串的公共子序列都为0
}
for(int i = 1;i<=n1;i++)
{
for(int j = 1;j<=n2;j++)
{
if(text1[i-1]==text2[j-1])//error2:判断i-1与j-1就是判断i,j的值,这个一定要切记。因为s是从0开始,而dp是从1开始的,dp的0代表的是空串
{
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];
}
};
4. 连续子数组的最大和
题目
方法一
code
class Solution {
public:
int maxSubArray(vector<int>& nums) {
//动态规划,三步
//1、dp[i] 代表到包含nums[i]的子数组的最大和
//2、动态转移方程
//3、初始化
int n = nums.size();
vector<int> dp(n);
dp[0] = nums[0];
int res = dp[0];
for(int i = 1;i<n;i++)
{
if(dp[i-1]<0)
dp[i] = nums[i];
else
{
dp[i] = dp[i-1]+nums[i];
}
res = max(res,dp[i]);
}
return res;
}
};
进阶
code
方法二
code
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n==0)
return 0;
int s= 0;
int res = INT_MIN;//要考虑输入只有一个负数的情况
for(int i = 0;i<n;i++)
{
if(s<0)
s = 0 ;
s+=nums[i];
res = max(res,s);
}
return res;
}
};
5. 回文字符的数目
题目
我写的方法,但测试没通过,但在测试用例的地方都是通过的,就很奇怪。仅供参考。
code
class Solution {
public:
int countSubstrings(string s) {
if(s.size()==0)
return 0;
int n = s.size();
int dp[n][n];//代表第i个到第j个字符是否是回文字符
for(int i = 0;i<s.size();i++)
{
dp[i][i] = 1;//
}
for(int i = 0;i<n;i++)
{
dp[i][i]=true;//只有一个元素的情况,是不是每个元素都是回文串
}
for(int L = 2;L<=n;L++)//L代表的是子串的长度
{
for(int i = 0;i<n;i++)
{
int j = i+L-1;//j=1 dp[i][j]就是有两个元素
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];
}
}
}
}
int res = 0;
for(int i = 0;i<n;i++)
{
for(int j = 0;j<n;j++)
{
if(dp[i][j]==true)
{
res++;
}
}
}
return res;
}
};
6. 青蛙跳台阶
- 题目
- 解题步骤
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];
}
};
7. 机器人走到网格右下角
- 题目
- 解题步骤
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];
}
8. 最小路径和
- 题目
- 解题步骤
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];
}
};
9. 编辑距离
- 题目
- 解题步骤
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];
}
};
10. 零钱兑换
- 题目
- 解题步骤
- 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]
}
};
11. 最长递增子序列
- **题目
- 解题步骤
- 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;
}
}
12. 最长公共子序列
- 题目
- 解题步骤
- 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];
}
};
13. 高楼扔鸡蛋问题
14. 最长回文字符串
题目
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);
}
};