复习dp理论基础
1.动态规划:给定一个问题,我们把它拆解成一个个子问题,直到子问题可以直接被解决。然后把子问题的答案保存起来,以减少重复计算,再根据子问题找到原问题,直到找到原问题的解。
2.01背包:选择只可以使用一次的若干物品放到背包中以达到背包价值最大。遍历顺序倒序,先遍历物品,再遍历背包
3.完全背包:选择可以使用无限次的若干物品,放到背包中以达到背包价值最大,遍历顺序正序
4.多重背包:有N种物品和一个容量为 W 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Valule i 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
5.比较难想的问题都会让你几乎找不到背包的影子,但是它们都会折射出动态规划的思想
力扣516最长回文子序列
首先看到这道题的时候就要思考dp数组代表的含义,唯一和回文有关的就只能是dp[i][j]代表从i到j的最长回文子串长度。
接下来要思考如何遍历ij,使得ij可以寻找到最长回文子串。
“回文”串的满足条件是首尾相等,所以我们可以以此为遍历条件,如果si与sj相等,那么我们使dp[i+1]与dp[j-1]求得的回文长度+2。如果不相等,那么我们可以max(dp[i+1][j],dp[i][i-1]),这就是我们的递推公式。
至于为什么是i--而j++,是因为本题是01背包问题,如果是i++,j--那么会重复选取s中的字符。
int longestPalindromeSubseq(string s) {
int n=s.length();//注意求字符串的长度是length()
vector<vector<int>>dp(n,vector<int>(n));//定义dp数组
//dp[i][j]表示字符串s从i到j的最长回文子串的长度
for(int i=n-1;i>=0;--i)
{
dp[i][i]=1;//字符长度为1回文长度1
char c=s[i];//保存字符后续比较
for(int j=i+1;j<n;++j)
{
char c2=s[j];
if(c==c2)
{
dp[i][j]=dp[i+1][j-1]+2;//缩小寻找长度之后加上新的回文
}
else
{
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);//选择一种相对来说最大的回文子串
}
}
}
return dp[0][n-1];
}
题目类似于01背包,但是初始化、遍历顺序、不同条件下的递推公式、甚至于对于dp数组的定义,都相比于简单的01背包问题复杂很多,想要思维只能勤加练习。
力扣647回文子串
仔细思考题目几遍,没有思路。看到题解之后,醍醐灌顶,恍然大悟。
首先我们需要知道背包问题可以转换为dp数组值为布尔类型的情况,dp[i][j]的含义是字符串从i到j是true否false回文串
其次我们需要注意到递推公式某种程度上会决定遍历顺序
我们先说一下递推公式,其实本题的递推公式并不是以前那种背包问题,比如max或者加和,而是对于dp[i][j]的false/true的判断。当然这种判断是基于i与j的,也是基于s[i]==s[j](题设条件回文)的,如果i==j,那么例如‘a’,肯定是回文串,dp[i][j]=true;如果j-i==1,那么例如‘aa’肯定也是回文串,dp[i][j]=true;如果j-i>1那么就要思考,i与j夹着的那个dp[i+1][j-1]是否是回文子串,这也是动态规划的魅力所在。
为什么说递推公式会决定遍历顺序,因为dp[i][j]是由dp[i+1][j-1]决定的,也就是是由左下方决定的,那么我们的遍历顺序必须朝着右上方走,也就是i--&&j++
还有一个要注意的点就是本题的返回值(答案)不是dp[i][j],而是用result++来累积,所以不要死板理解动态规划问题
最后,附上参考理解后的代码:
int countSubstrings(string s) {
vector<vector<bool>>dp(s.size(),vector<bool>(s.size(),false));
int result=0;//存储结果
for(int i=s.size();i>=0;--i)
{
for(int j=i;j<s.size();++j)
{
if(s[i]==s[j])
{
if(j-i<=1)//一个回文或者两个相同字母
{
result++;
dp[i][j]=true;
}
else if(dp[i+1][j-1])//子串也是回文
{
result++;
dp[i][j]=true;
}
}
}
}
return result;
}