DP问题没见过很难理解,但是模型也很多,写题的时候可以多套用之前模型的思路。这里结合AcWing闫总跟代码随想录卡哥的思想去理解序列DP。
子序列问题
不连续子序列
最长上升子序列
题目大意:给定一个数组,找出其中最长严格递增的子序列长度。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
最基本的模型,需要理解后把板子记下来。
1.dp数组定义:所有以i结尾的上升子序列的集合。性质:最长(最大max)
2.递推公式分析:看i之前的数,有0 ~ i-1个数,可以以此为划分依据。但要满足上升子序列的要求还需要满足a[j] < a[i] (j属于[0,i-1])。如果满足条件 f[ i ] = f [ j ] +1。
核心代码:两重循环,第一重枚举所有i,第二重枚举i之前的数,也就是0 ~ i - 1的数
for(int i =0;i<n;i++)
{
dp[i] = 1; //相当于初始化操作,只有i一个数的时候 最长上升子序列是本身,为1
for(int j =0;j<i;j++) //j不能等于i
if(a[j]<a[i])
dp[i] = max(dp[i],dp[j]+1);
}
3.dp数组初始化:当以i结尾的数只有i一个数的时候,f[i] = 1
4.遍历顺序:从前往后遍历即可
5. 打印dp数组:这里需要注意,dp[i] 的含义是以i结尾的最长上升子序列,但dp[i]不一定是所有中最长的,所以最后循环一边f[0]到f[n-1]取最大值就可以了。
code:
#include<iostream>
using namespace std;
const int N = 1005;
int a[N];
int dp[N];
int main()
{
int n;
cin>>n;
for(int i = 0;i<n;i++) cin>>a[i];
for(int i =0;i<n;i++)
{
dp[i] = 1; //相当于初始化操作,只有i一个数的时候 最长上升子序列是本身,为1
for(int j =0;j<i;j++) //j不能等于i
if(a[j]<a[i])
dp[i] = max(dp[i],dp[j]+1);
}
int ans = -1;
for(int i = 0;i<n;i++) ans = max(ans,dp[i]);
cout<<ans<<endl;
return 0;
}
最长公共子序列
题目大意:给定两个序列,求既是序列a的子序列和b的子序列的字符串最长长度。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组含义:dp[i][j] 表示所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列的集合。性质:长度最大。
2.递推公式分析:看最后一个元素,考虑第i个字母以及第j个字母是否满足条件。
这里其实01和10的情况包括了00的情况,但求的是最大值,所有对结果没有影响的。
dp[i-1][j-1]选到是由要求的:a[ i ] == b[ j ] 只有相同了才可以
核心代码:
for(int i = 1;i<n;i++)
for(int j = 1;j<m;j++)
{
if(text1[i]==text2[j])
f[i][j] = max(f[i][j],f[i-1][j-1]+1);
else
f[i][j] = max(f[i-1][j],f[i][j-1]);
}
3.初始化:因为出现了i-1跟j-1,而i,j是从1开始的,所以要考虑0的情况。这里f[0][0]表示a,b都是空序列的情况,那自然f[0][0] = 0其他的f[i][j]取值只要不影响max判断即可,,所以全初始化为0即可。
4.遍历顺序:
画图可知要先用到i-1,j-1的情况,所以从上往下从左往右遍历即可。
code:这里是力扣上面的代码,我喜欢从输入的下标都从1开始,所以加了点初始化操作
class Solution {
public:
int f[1005][1005];
int longestCommonSubsequence(string text1, string text2) {
text1.insert(text1.begin(),0);
text2.insert(text2.begin(),0);
int n = text1.size();
int m = text2.size();
for(int i = 1;i<n;i++)
for(int j = 1;j<m;j++)
{
if(text1[i]==text2[j])
f[i][j] = max(f[i][j],f[i-1][j-1]+1);
else
f[i][j] = max(f[i-1][j],f[i][j-1]);
}
return f[n-1][m-1];
}
};
不相交的线
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
code:
class Solution {
public:
int f[505][505];
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
nums1.insert(nums1.begin(),0);
nums2.insert(nums2.begin(),0);
int n = nums1.size();
int m = nums2.size();
for(int i = 1;i<n;i++)
for(int j = 1;j<m;j++)
{
f[i][j] = max(f[i-1][j],f[i][j-1]);
if(nums1[i] == nums2[j])
f[i][j] = max(f[i][j],f[i-1][j-1]+1);
}
return f[n-1][m-1];
}
};
连续子序列
最长连续递增序列
题目大意:给定一个数组,求其中最长的连续递增序列。
与最长上升子序列的区别:子序列可以是不连续的。
与最长上升子序列很像,区别在于递推公式。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组含义:所以以i结尾的连续子序列的长度的集合。性质:最大值max
2.递推公式:还是看最后一个字母,如果a[i] < a[i+1]的话,f[i+1] = f[i] +1 。
如何保证连续性呢?在最长上升子序列中,我们其实是枚举了很多的子序列,因为两次循环,第一次枚举结尾,第二次枚举确定结尾后有的子序列。要保证是连续的,就用一次循环,从头到尾遍历即可。
3.初始化:f[i] = 1,表示如果只有自己一个元素的话长度就为1
4.遍历顺序:从左向右
code:同理,f[i]并不一定是我们要的最大值,只是以i结尾的最大值
class Solution {
public:
int dp[10004];
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
fill(dp,dp+n,1);
for(int i = 0;i<n-1;i++)
if(nums[i+1]>nums[i])
dp[i+1] = dp[i] + 1;
int ans = 0;
for(int i = 0;i<n;i++) ans = max(ans,dp[i]);
return ans;
}
};
最长重复子数组
题目大意:给定两个数组,求两个数组中最长的公共子数组。注意:子数组是要求连续的
1.dp数组含义:dp[i][j]表示a数组中以i结尾,b数组中j结尾的公共子数组长度。性质:Max
2.确定递推公式:如果a[i] == b[j]的话,f[i][j] = max(f[i-1][j-1]+1,f[i][j]),如果不相等呢?不相等说明已经不可以构成两个相等的子数组了,直接赋0即可 f[i][j] = 0
3.初始化:f[0][0],两个空数组的公共子数组长度为0
4.遍历顺序:两个数组从左往右即可
这里有个疑问,为什么不按照之前最长公共子序列的定义方法呢?dp[i][j]表示a数组中从0-i,b数组中从0-j的公共子数组的最大值?因为无法保证连续性。但是结尾的定义可以。为什么?因为a[i]!=b[j]的话就会等于0了,0是什么?0是初始化的值。这就保证了不连续。如果按照0-i,0-j的定义,f[][]的值一直在上升,不会回到0,那按照定义岂不是都是公共子数组了?显示不是。
code:
class Solution {
public:
int f[1005][1005];
int findLength(vector<int>& nums1, vector<int>& nums2) {
nums1.emplace(nums1.begin(),0);
nums2.emplace(nums2.begin(),0);
int n = nums1.size();
int m = nums2.size();
for(int i = 1;i<n;i++)
{
for(int j = 1 ;j<m;j++)
{
if(nums1[i] == nums2[j])
f[i][j] = max(f[i][j],f[i-1][j-1] +1);
else
f[i][j] = 0;
}
}
int ans =0;
for(int i =0;i<n;i++)
for(int j = 0;j<m;j++)
ans = max(ans,f[i][j]);
return ans;
}
};
最大子序和
题目大意:给定一段数组,求其中连续的子数组的和最大值
1.dp数组定义:dp[i]表示以i结尾的子数组的和的集合。性质:和的最大值
2.递推公式:看最后一个数即可。如果已经循环到a[i],之前的最大值f[i-1]加上a[i]后还没有f[i-1]大,那么f[i]就应该回到0,加上a[i],这样保证连续性。 f[i] = max(f[i-1]+a[i],a[i]);
3.初始化: f[0]表示一个数都没有的时候的值就为0
4.遍历顺序:从左向右
code:
class Solution {
public:
int f[100005];
int maxSubArray(vector<int>& nums) {
nums.insert(nums.begin(),0);
int n = nums.size();
for(int i = 1;i<n;i++)
f[i] = max(f[i-1] + nums[i],nums[i]);
int ans = INT_MIN;
for(int i = 1;i<n;i++) ans = max(f[i],ans);
return ans;
}
};
总结:由于是连续的子序列,如何体现连续性呢?首先dp数组定义:一般都是以最后一个数作定义。dp数组不能一直增加,如果不满足状态就应该从初始化开始再变化。最后取一个最值。
编辑距离问题
判断子序列
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题目大意:给定连个子序列a,b。问a是否是b的一个子序列。
其实是一个很经典的双指针问题,都知道是双指针问题,但是如何证明正确性是有难度的。这里介绍动态规划的方法。
1. 定义dp数组:dp[i][j]:a中前i个字符,b中前j个字符 中的公共字符长度的集合。性质:max
2.递推公式: 分析a,b中最后一个字符的情况,分为两类:
① a【i】 == b【j】 这种的话 f[ i ][ j ] = max(f[ i ][ j ] , f[ i -1][j - 1] + 1)
② a【i】!=b【j】 这种就要考虑能不能b删除一个字符来满足情况:f[ i ][ j ] = max(f[ i ][ j ],f[ i ][ j - 1]) 。 f[ i ][ j -1]表示考虑a中前i个,b中前j-1个,也就是b中删除一个的情况。
3.初始化: f[ 0 ][ 0 ] = 0。a为空,b为空的时候,没有相同的部分。
4.遍历顺序,从左往右。
code: 最后只要判断一下f[n-1][m-1]是否与n-1的个数相等即可
class Solution {
public:
int f[105][1005];
bool isSubsequence(string s, string t) {
s.insert(s.begin(),'0');
t.insert(t.begin(),'0');
int n = s.size();
int m = t.size();
for(int i = 1;i<n;i++)
for(int j = 1;j<m;j++)
{
if(s[i] == t[j])
f[i][j] = max(f[i-1][j-1]+1,f[i][j]) ;
else f[i][j] = max(f[i][j],f[i][j-1]);
}
return f[n-1][m-1] == n-1;
}
};
不同的子序列
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题目大意:给定字符串s,t 求 t在s的子序列中出现过几次。
换句话说,s中有几种删除字母的方式可以得到t,就出现过几次。
1. dp数组定义: dp[i][j] s中以i结尾,t中以j结尾的序列,s可以通过删除字母的方式可以得到t的方法集合。 性质:数目(count)。 注意这里是删除方法的集合。
2. 递推公式:分析最后一个字母情况。分为两种情况:
① 如果s[i] == t[j] f[i][j] = f[i-1][j-1] + f[i-1][j]。 这里f[i-1][j-1]都好理解,直接从i,j上一个状态加过来就行了,为什么还要加一遍f[i-1][j]呢 例如 “abcc”与“abc” 当最后一个字母相同的时候,“abcc”可以删除第一个c也可以删除第二个c去匹配"abc",所以要加上。
②如果s[i]!=t[j],这个时候就要考虑下删除下i,即不选i,看下s[i-1]与t[j]能否匹配了 f[ i ][ j ] = f[ i - 1 ][ j ]
3.初始化:这里要考虑下了,f[i][0] 一定都为1,为什么?s有i位,t为空串,只有一种方法从s到t,就是全删完。f[0][j]一定都为0,表示s为空串,不可能删除得到t了。那f[0][0]呢?应该为1,删除0个元素从空串到空串。
4.遍历顺序:从左往右。
code:
class Solution {
public:
uint64_t f[1005][1005];
int numDistinct(string s, string t) {
s.insert(s.begin(),'1');
t.insert(t.begin(),'1');
int n = s.size();
int m = t.size();
for(int i = 0;i<n;i++) f[i][0] = 1;
for(int j = 1;j<m;j++) f[0][j] = 0;
for(int i = 1;i<n;i++)
for(int j = 1;j<m;j++)
{
if(s[i] == t[j]) f[i][j] = f[i-1][j] + f[i-1][j-1];
else f[i][j] = f[i-1][j];
}
return f[n-1][m-1];
}
};
那 判断子序列 中是否也要考虑最后一个字符相等的情况下,但是可以选择删除s的最后一个字符而使s变为t呢?因为求的是最大值,其实加上也可以 f[i][j] = max({f[i-1][j-1]+1,f[i][j],f[i][j-1]}) 也是对的。
两个字符串的删除操作
题目大意:给定两个字符串,在都可以删除字母的情况下,问最少几步可以让两个字符串相等。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组定义:dp[i][j]:s串中0-i,t串0-j中通过删除使两个字符串相等的步数的集合。性质:最小步数(Min)
2.递推公式:按照最后一个字母分析:
① 如果最后一个字符相等: s[ i ] == t[ j ] 那说明不用删除两串中的一个字符就能相等,所以最小值从 f[i-1][j-1]中选取即可。 f[i][j] = min(f[i][j],f[i-1][j-1]);
②如果最后一个字符不相等,那需要删除一个字符让两者相等,可以选择删s[i]也可以删除t[j],不论删除哪个最小步数都要加一。 f[i][j] = min(f[i-1][j],f[i][j-1])+1;
3.初始化数组:f[i][0] 表示s串有i个字符,t串只有0个字符,要想两者最后相等只有把s删完,所以步数就为i。同理f[0][i] = i。由于求的是最小值,所以一开始的初始化要不影响后面的取min操作,所以其余全部初始化为正无穷即可。
4.遍历顺序:从左往右。
code:
class Solution {
public:
int f[505][505];
int minDistance(string w1, string w2) {
w1.insert(w1.begin(),'0');
w2.insert(w2.begin(),'0');
int n = w1.size(),m = w2.size();
memset(f,0x3f,sizeof f);
for(int i = 0;i<n;i++) f[i][0] = i;
for(int i = 0;i<m;i++) f[0][i] = i;
for(int i = 1;i<n;i++)
for(int j = 1;j<m;j++)
{
if(w1[i]==w2[j])
f[i][j] = min(f[i][j],f[i-1][j-1]);
else
f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
}
return f[n-1][m-1];
}
};
编辑距离
题目大意:给定两个字符串s,t。对s可以有三种操作:删一个字母,加一个字母,改一个字母。求从s变为t的最小操作数。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组分析:dp[i][j]表示从s串中0-i,t串中0-j通过编辑操作使s变为t的操作数的集合。性质:最小操作数(min)
2.递推公式分析:考虑最后一个字符的三种操作:
①如果 s[ i ] == t[ j ] 那么最小值就是与原值取min即可。 f[ i ][ j ] = min(f[ i ] [j ],f[ i -1][ j - 1]);
②如果 s[ i ] != t[ j ] 那么就可以有三种操作了:
a.删s中的一个字母:说明s的前i-1个字母与t前j个字母相等,那就是f[i-1][j] + 1;
b.给s加一个字母:说明加之前s前i个与t的前j-1个字母相等,那就是f[i][j-1] + 1;(其实也是相当于给t中删一个字母)
c.改s的最后一个字母:只差最后一个字母相同,说明s的前i-1个字母与t中的前j-1个字母是相等的。那就是f[i-1][j-1]+1
综上所述:那就是s[ i ] != t[ j ]的情况下有三种情况,取最小即可:f[i][j] = min(f[i-1][j],f[i][j-1],f[i-1][j-1])+1;
3.初始化操作:由于取min,为了不干扰取min操作,一开始全部赋值为正无穷即可。由于要用到f[i][j-1]与f[i-1][j],那就要考虑f[0][i]与f[i][0]了。
f[i][0]:表示s中子串长度为i,t为空串。根据dp数组分析,需要变成空串需要i步。
f[0][j]: 表示s为空串,t为长度为j的串。我们需要j步才可以把s变成t。
4.遍历顺序:从左往右即可。
code:
class Solution {
public:
int f[505][505];
int minDistance(string w1, string w2) {
w1.insert(w1.begin(),'0');
w2.insert(w2.begin(),'0');
int n = w1.size(),m = w2.size();
memset(f,0x3f,sizeof f);
for(int i = 0;i<n;i++) f[i][0] = i;
for(int j = 0;j<m;j++) f[0][j] = j;
for(int i =1;i<n;i++)
for(int j = 1;j<m;j++)
{
if(w1[i] == w2[j])
f[i][j] = min(f[i-1][j-1],f[i][j]);
else
f[i][j] = min({f[i-1][j],f[i][j-1],f[i-1][j-1]})+1;
}
return f[n-1][m-1];
}
};
回文子串问题
回文子串的问题一开始很难想,但是感觉模板就是那样的,很好套用。
回文子串
问题大意:给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组定义:dp[ i ][ j ] 表示一个字符串中第i位到第j位是否是回文子串的集合。性质:True/False。
2.递推公式分析:
① 如果开头的字母与结尾的字母相同:即s[i+1] == s[j-1]:
a. 如果i与j是相等的: 例如:“a” 这个时候说明是一个回文子串。
b.如果j = i + 1:例如 “aa” 这个时候也是一个回文子串。
c.如果j>i+1:例如 “abba” 和 “abca”这个时候是否是一个回文子串就要判断一下f[i-1][j+1]是否是True,如果是True,那就是回文子串,如果不是,那就不是回文子串。
② 如果开头与结尾的字母不相同: 即s[i+1] != s[j-1]:说明肯定不是回文子串,直接False即可。
综上所述: 若 s[i + 1] == s[ j - 1] 看j与i的关系继续判断,若不相等,则直接为False
3. 初始化操作: f[0][0]表示空字符串,那肯定不是回文字符串,所以全初始化为0即可。
4.遍历顺序: 回文子串的递推公式感觉是有规律的。因为是用到f[i + 1][ j - 1],但是这个数是在f[i][j]的左下角的,也就是说i要从下往上遍历,j还是从左向右遍历的。那j还是从0开始遍历吗?
其实这里我感觉i和j相当于枚举了左右端点,i是左端点,j是右端点。所以j是从i开始循环的,一直到size()-1结束。
code:这里如果是True,则说明这一段是回文子串,ans++即可。由于枚举了所有的左右端点,所以是不重不漏的。
class Solution {
public:
bool f[1005][1005];
int ans;
int countSubstrings(string s) {
int n = s.size();
for(int i = n-1;i>=0;i--)
for(int j = i;j<n;j++)
{
if(s[i] == s[j])
{
if(j-i<=1)
f[i][j] = 1,ans++;
else
if(f[i+1][j-1]) ans++,f[i][j] = 1;
}
else f[i][j] =0;
}
return ans;
}
};
最长回文子串
题目大意:找出一个回文串中最长的子串。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组定义: dp[i][j]表示一个字符串中从i到j是否是回文子串的集合。性质:True/False
2.递推公式分析:与上题相同。
3.初始化:与上题相同。
4.遍历顺序:与上题相同。
code: 不同的是要返回最长子串。如何记录呢?在每次判断后再加个判断:如果这一段是回文子串的话,且这次的回文子串的长度比上一次要长的话,我们就更新一下最长子串。 由于我们记录了左右端点的下标,长度可以用r-l+1表示出来。同理我们如果知道了区间长度len,和左端点l,右端点也可以用长度跟l表示出来的: r = len+l-1。区间dp的时候要这样考虑的。
class Solution {
public:
bool f[1005][1005];
string longestPalindrome(string s) {
int n = s.size();
if(n==0||n==1) return s;
int maxlen = 0,lidx = 0;
for(int i = n-1;i>=0;i--)
for(int j = i;j<n;j++)
{
if(s[i] == s[j])
{
if(j-i<=1) f[i][j] = 1;
else
if(f[i+1][j-1]) f[i][j] = 1;
}
else
f[i+1][j-1] = 0;
if(f[i][j] == 1 && j-i+1>maxlen)
{
lidx = i;
maxlen = j-i+1;
}
}
return s.substr(lidx,maxlen);
}
};
密码脱落
题目大意:给定一个字符串,问最少添加几个字符可以让它变为回文串
1.dp数组定义:dp[i][j]:表示一个串中从i到j的子串通过添加字符的方式变成回文串的步数集合。性质:最小步数(Min)
2.递推公式分析: 看最后一个字符:s[i]与s[j]
①若s[ i ] == s[ j ]:
a.若j-i<=1:说明是"aa"或者是"a"的情况,那f[i][j] = 0即可
b.若j-i>1,那就要看s[i+1]与s[j-1]的情况了,与f[i+1][j-1]取Min
②若s[ i ] != s[ j ]: 若原来是回文串,那就要添加字符了,可以添加在头或者是尾
f[ i ][ j ] = min(f[ i + 1 ][ j ],f[ i ][ j - 1 ])+1,+1表示操作数要加一了。
3.初始化:全部初始化为正无穷即可。
4.遍历顺序:老规矩:i从大到小,j从i到n。
code:
#include<iostream>
#include <cstring>
using namespace std;
int f[1005][1005];
int main()
{
string s;
cin>>s;
int n = s.size();
memset(f,0x3f,sizeof f);
for(int i = n-1;i> -1;i--)
{
for(int j = i;j<n;j++)
{
if(s[i] == s[j])
{
if(j-i <=1 ) f[i][j] = 0;
else f[i][j] = min(f[i][j],f[i+1][j-1]);
}
else
f[i][j] = min(f[i+1][j],f[i][j-1])+1;
}
}
cout<<f[0][n-1]<<endl;
return 0;
}
最长回文子序列
题目大意:给应一个字符串,求对于的回文子序列最大值。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
1.dp数组定义:dp[i][j]:表示从串从i到j回文子序列的长度的集合。性质:Max
2.递推公式分析:还是看s[i]与s[j]:
①s[i] == s[j]:
a. i == j 说明是一个字符,肯定是回文的。f[i][j] = 1;
b. j==i+1 说明是"aa",f[ i ][ j ] =2;
c. i != j 说明要在之前的f[i+1][j-1]的基础上长度加上2 ,f[i][j] = f[i+1][j-1] +2;
②s[i] != s[j]:
这就要看一下f[i+1][j]与f[i][j-1]谁大了: f [ i ][ j ] = max(f[ i + 1 ][ j ],f[ i ][ j - 1 ]);
3.初始化:求最大值,初始化为0即可。
4.遍历顺序:老规矩:i从大到小,j从i到n。
code
class Solution {
public:
int f[1005][1005];
int longestPalindromeSubseq(string s) {
int n = s.size();
for(int i = n-1;i>=0;i--)
for(int j = i;j<n;j++)
{
if(s[i] == s[j])
{
if(j==i)
f[i][j] = 1;
else if(j == i+1)
f[i][j] = 2;
else
f[i][j] = f[i+1][j-1] + 2;
}
else
f[i][j] = max(f[i+1][j],f[i][j-1]);
}
return f[0][n-1];
}
};
至此,暑假写的一些序列dp问题写完了,还有很多dp模型:区间dp,背包dp等等,有时间一定要记录。算法初学者,不喜勿喷。