五步:
-
dp数组以及下标的含义
-
递推公式
-
dp数组如何初始化
-
遍历顺序(eg 背包问题)
-
打印dp数组(检查打印出来的数值是否符合预期)
(一)斐波那契数
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
int dp[2];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i ++){
int t = i % 2;
dp[t] = dp[0] + dp[1];
}
return dp[n%2];
}
};
class Solution {
public:
int fib(int n) {
if(n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
};
(二)爬楼梯
- 确定dp数组及下标的含义
dp[i]: 爬到第i层楼梯,有dp[i]种方法
- 确定递推公式
在到达第n层的上一步,我们只有两个选择,走一步,或者走两步。 如果是走一步,我们需要先通过 f(n-1)种方式到达 n-1 层 如果是走两步, 我们需要通过 f(n-2)种方式到达第 n - 2 层
综上有 f(n) = f(n-2) + f(n-1)
- dp数组初始化
题目中n为正整数,所以本题其实就不应该讨论dp[0]的初始化!
初始化dp[1] = 1,dp[2] = 2
- 确定遍历顺序
从前向后遍历
- 举例推导dp数组
1 2 3 5 8
class Solution {
public:
int climbStairs(int n) {
if(n <= 3) return n;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
for(int i = 4; i <= n; i ++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
(三)使用最小花费爬楼梯
- 确定dp数组以及下标的含义
dp[i]: 到达第i台阶所花费的最少体力
- 确定递推公式
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。
dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。
选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
- dp数组初始化
到达第0和1个台阶式无花费的,但是从第0或1个台阶向上跳,则需要相应的花费
所以初始化 dp[0] = 0,dp[1] = 0
- 确定遍历顺序
从前向后
- 打印dp数组
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size() + 1);
dp[0] = 0;
dp[1] = 0;
//注意这里是跳到第cost.size()层,因为cost[cost.size() - 1]的数据还可以被用到
for(int i = 2; i <= cost.size(); i ++){
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
//顶层是cost.size()层
return dp[cost.size()];
}
};
(四)不同路径
- 确定dp数组及下标的含义
dp[i][j]: 从【0,0】位置走到【i,j】有多少条不同的路径
- 确定递推公式
上一步可能处在的位置 dp[i - 1][j] 或者dp[i][j - 1]
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
- dp数组初始化
因为是从左往右、从上往下推导,因此要初始化最左边一列和最上面一行
dp[i][0] 和 dp[0][j],都只有一种走法(只能往右或往下走)
dp[i][0] = 1(0 ≤ i < m) , dp[0][j] = 1(0 ≤ j < n)
- 确定遍历顺序
从左往右、从上往下(因为初始值在上面和左边)
- 打印dp数组
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 0));
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];
}
};
(五)不同路径II
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
//初始化部分,如果遇到障碍物,后面的路径数量都是0,而不是初始化为1
for(int i = 0; i < m && obstacleGrid[i][0] != 1; i ++) dp[i][0] = 1;
for(int j = 0; j < n && obstacleGrid[0][j] != 1; j ++) dp[0][j] = 1;
for(int i = 1; i < m; i ++){
for(int j = 1; j < n; j ++){
//注意这里只有没有障碍物的格子才重新计算dp[i][j],有障碍物数值还是0,就是没有通过路径
if(obstacleGrid[i][j] == 0){
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
};
(六)整数拆分
数值近似相等,乘积才尽可能大
- 确定dp数组及下标的含义
dp[i]:拆分数字i,得到的最大乘积为dp[i]
- 确定递推公式
拆成两个数,j × (i - j)
拆成三个及以上的数 j × dp[i - j](这里默认一定将i - j拆分成大于2个数)
- dp数组初始化
dp[0] 无意义 dp[0] = 0, 这里给初始化为0,因为0乘以任何数都为0,不影响最大值
dp[1]无意义 dp[1] = 0,同上
dp[2] = 1;
- 遍历顺序
for(i = 3; i <= n; i ++){
for(j = 1; j < i; j ++){
//优化,j <= i/2即可,因为拆成尽可能近似相同数,才可能得到最大的乘积
//例如:100拆成2个数,为50*50,拆分成3个数,为33*33*34,拆成4个数,为25*25*25,...因此遍历到i/2即可
//放入dp[i]的原因:这里是试探划分i为j和i - j, 比较得到乘积最大的划分,就是要保留dp[i]最大的结果
dp[i] = max(j *(i - j), j *dp[i - j], dp[i]);
- 打印dp数组
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
dp[2] = 1;
for(int i = 3; i <= n; i ++){
for(int j = 1; j <= i/2; j ++){
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
};
(七)不同的二叉搜索树
n为3的二叉树种类=头结点为1的二叉树个数+头结点为2的二叉树个数+头结点为3的二叉树个数
头结点为1的二叉树个数 = 左子树0个节点(n = 0) * 右子树2个节点(n = 2)
头结点为2的二叉树个数 = 左子树1节点(n = 1) * 右子树1个节点(n = 1)
头结点为3的二叉树个数 = 左子树2节点(n = 2) * 右子树0个节点(n = 0)
dp[3] = dp[0]*dp[2] + dp[1]*dp[1] + dp[2] * dp[0]
- 确定dp数组及下标含义
dp[i]: 1到i个不同元素节点的二叉搜索树的个数为dp[i]
- 确定递推公式
dp[i]
以j为头结点 左子树节点:j - 1 右子树节点:i - j,共有dp[j - 1]*dp[i - j]种二叉树
将不同的j为头结点的二叉树种类数相加
dp[i] += dp[j - 1] * dp[i - j] ( 1≤ j ≤ i)
- dp数组初始化
dp[0] = 1
dp[1] = 1(可以不初始化了)
dp[i] = 0;
- 遍历顺序
后面的依靠前面的数值,i从小到大遍历
for(i = 1; i <= n; i ++){
for(j = 1; j <= i; j ++){
dp[i] += dp[j - 1]*dp[i - j];
}
}
- 打印dp数组
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= i; j ++){
// 注意这里是累加
dp[i] += dp[j - 1]*dp[i - j];
}
}
return dp[n];
}
};
(八)01背包基础理论
暴力解法:回溯,每个物品两种状态(取/不取),时间复杂度 2^n
- 确定dp数组的含义
dp[i][j]: i表示物品数目,j表示背包当前剩余容量,dp[i][j]表示背包当前最大价值
- 确定递推公式
注意要先判断是否能放入该物品,如果当前剩余量j < weight[i],则只要考虑情况1
情况1,如果不放物品i:背包内物品最大价值为dp[i - 1][j]
情况2,如果放物品i:背包内物品最大价值为dp[i][j - weight[i]] + value[i]
取两种情况的最大值:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
- dp数组初始化
由于dp[i][j]是由其上方元素或者左上方元素得到的,因此初始化左边一列和上面一行
其他初始化无影响
- 遍历顺序
对于二维数组解决01背包,物品和背包的遍历顺序不影响(可以先for循环遍历物品/背包,再for循环遍历背包/物品)
- 打印dp数组
#include <iostream>
using namespace std;
#include <vector>
int main(){
int m, n;
cin >> m >> n;
vector<int> space(m + 1);
vector<int> value(m + 1);
for(int i = 0; i < m; i ++){
cin >> space[i];
}
for(int i = 0; i < m; i ++){
cin >> value[i];
}
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for(int j = space[0]; j <= n; j ++){
dp[0][j] = value[0];
}
for(int i = 1; i < m; i ++){
for(int j = 1; j <= n; j ++){
//如果不能装入该物品
if(j < space[i]) dp[i][j]= dp[i - 1][j];
// 如果能转入该物品
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + value[i]);
}
}
//注意这里是m-1
cout << dp[m - 1][n];
return 0;
}
补充:
将二维dp数组降维一维
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
第i行由第 i - 1行推导得来,直接将上一层拷贝到当前层,每次计算都更新当前层的数据
- 确定dp数组的含义
dp[j]:容量为j的背包所能装的最大价值为dp[j]
- 递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
- 初始化
dp[0] = 0
其他的因为要取max,因此都初始化为0即可
- 遍历顺序
背包倒叙遍历,确保每个物品最多只能被放入一次
只能先遍历数组,再遍历背包
for(i = 0; i < 物品数量; i ++) //物品
for(j = bagweight; j >= weight[i]; j --) //背包倒叙遍历
//递推公式
- 打印dp数组
(九)分割等和子集
转换为找到集合里是否能够找到总和为sum/2的子集。(递归问题)
- 确定dp数组及其含义
dp[i][j]:前 i 个数的子集合里能找到总和为 j 的子集
- 递推公式
不选num[i],则dp[i][j] = dp[i - 1][j]
选num[i], 则dp[i][j] = dp[i - 1][j - num[i]] (如果前i - 1个数的集合可以取到总和为j - num[i] 的子集,那么前i个数的集合可以取到总和为 j 的子集)
两种情况为或的关系
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num[i]]
- 初始化
第一列全部初始化为True,第一行第num[i]个初始化True(如果sum/2 ≥ nums[i]),其余全部初始化为false
- 遍历顺序
从左向右,从上向下初始化
- 打印dp数组
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int num : nums) sum += num;
if (sum % 2 != 0) return false; // 如果总和是奇数,无法平分
int W = sum / 2; // 背包容量设置为总和的一半
vector<vector<bool>> dp(nums.size() + 1, vector<bool>(W + 1, false));
// 总和为0,任何子集都为true
for(int i = 0; i < nums.size(); i ++){
dp[i][0] = true;
}
if(W >= nums[0]) dp[0][nums[0]] = true;
for(int i = 1; i < nums.size(); i ++){
for(int j = 1; j <= W; j ++){
if(j < nums[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
return dp[nums.size() - 1][W];
}
};
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int num : nums) sum += num;
if (sum % 2 != 0) return false; // 如果总和是奇数,无法平分
int W = sum / 2; // 背包容量设置为总和的一半
vector<bool> dp(W + 1, false);
dp[0] = true; // 初始状态,容量为0时,可以形成和为0的子集
for (int num : nums) {
for (int i = W; i >= num; --i) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[W]; // 如果可以形成和为W的子集,返回true
}
};
(十)最后一块石头的重量II
每一次抵消,实际上都是[a, b, …]变成[a - b, …]或[b-a, …]的过程,最终也必然会得出类似 (b - a) - ( c - d) 这种计算式,展平得 -a + b - c + d = (b+d)-(a+c)这种计算式,b+d >= sum/2 >= a+c,需要做的就是找出a+c不大于sum/2时的最大值,经典背包问题
类比上一道分割等和子集
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int i = 0; i < stones.size(); i ++){
sum += stones[i];
}
int w = sum/2;
vector<int> dp(w + 1, 0);
for(int i = 0; i < stones.size(); i ++){
for(int j = w; j >= stones[i]; j --){
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - 2*dp[w];
}
};
(十一)目标和
-a + b = S
a + b = sum
2b = S + sum
b = (sum + S)/2
返回和为 b 的子集数目(组合问题),注意向下取整的影响,如果(S + sum) % 2 == 1,就是无解的
方法一:回溯
方法二:01背包
- 确定dp数组及下标的含义
dp[j]: 填满容量为 j 的包有dp[j]种方法
- 确定递推公式
凑成dp[j]有dp[j - nums[i]]种方法,将所有的dp[j - nums[i]]累加起来,得到方法总和
dp[j] += dp[j - nums[i]]
dp【j】 = dp【j】 + dp[j - nums【i】]
-
dp【j】: 不拿物品i,只考虑前 i-1 个物品,得到的装满容量为 j 的背包的方法数;
-
dp[j - nums【i】]: 拿物品i的话, 余的容量为 j - nums【i】,这种情况下需要考虑前 i-1 个物品,得到的装满剩余容量的背包的方法数。
- dp数组初始化
dp[0] = 1
凑到总和为0,有一种方法,就是所有物品都不选
nums={0},target=0,容易得到结果为2.又因为left=0,则dp【left】+=dp[left-nums【i】],即dp【0】+=dp【0-0】,即2dp【0】=2,得出dp【0】=1.
- 确定遍历顺序
先遍历物品,再遍历容量
- 打印dp数组
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int i = 0; i < nums.size(); i ++){
sum += nums[i];
}
if((target + sum)%2 != 0) return 0;
// 如果不能整除2,说明一定不可能取到
int w = (target + sum)/2;
// 需要选取一个子集,子集的和为w,这里w不能超过总和,或者小于0,否则不可能取到
if(w > sum || w < 0) return 0;
vector<int> dp(w + 1, 0);
dp[0] = 1;
for(int i = 0; i < nums.size(); i ++){
for(int j = w; j >= nums[i]; j --){
dp[j] += dp[j - nums[i]];
}
}
return dp[w];
}
};
(十二)一和零
01背包问题,只是物品的重量有两个维度,字符串本身的个数相当于物品的价值。
- 确定dp数组及其下标含义
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
- 确定递推公式
dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。
dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。
然后我们在遍历的过程中,取dp[i][j]的最大值。
所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
- dp数组初始化
初始化为0
- 确定遍历顺序
物品就是strs里的字符串,背包容量就是题目描述中的m和n。
物品从前往后遍历,背包容量从大到小遍历
- 打印dp数组
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for(string s: strs){ // 遍历物品
int zeroNum = 0;
int oneNum = 0;
for(int i = 0; i < s.size(); i ++){
if(s[i] == '1') oneNum ++;
else if(s[i] == '0') zeroNum ++;
}
for(int i = m; i >= zeroNum; i --){// 遍历背包容量从后往前遍历
for(int j = n; j >= oneNum; j --){
dp[i][j] = max(dp[i - zeroNum][j - oneNum] + 1, dp[i][j]);
}
}
}
return dp[m][n];
}
};
(十三)完全背包理论
每个商品有无限个
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
#include <iostream>
using namespace std;
#include <vector>
int main(){
int n, v;
cin >> n >> v;
vector<int> weight(n + 1);
vector<int> value(n + 1);
for(int i = 0; i < n; i ++){
cin >> weight[i] >> value[i];
}
vector<int> dp(v + 1, 0);
for(int i = 0; i < n; i ++){
for(int j = weight[i]; j <= v; j ++){
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[v];
return 0;
}
(十四)零钱兑换II
硬币数量不限 → 完全背包
求组合数 → 组合问题
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for(int i = 0; i < coins.size(); i ++){
for(int j = coins[i]; j <= amount; j ++){
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
(十五)组合总和IV
完全背包 + 排列问题
组合不强调顺序,(1,5)和(5,1)是同一个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
//排列问题,先遍历背包容量,在物品
for(int i = 0; i <= target; i ++){//背包容量
for(int j = 0; j < nums.size(); j ++){//物品
//保证结果一定在int范围,但是中间可能有超过int范围的,直接剔除这种可能
//注意不能直接写 dp[i] + dp[i - nums[j]] < INT_MAX,这样写还是将前两者相加转换为int型,还是超过了范围
//对背包容量做限制
if(i >= nums[j] && dp[i] < INT_MAX - dp[i - nums[j]]) dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
};
(十六)爬楼梯(进阶版)
排列问题 + 完全背包
(先遍历背包容量,再遍历物品) (从前向后遍历)
#include <iostream>
using namespace std;
#include <vector>
int main(){
int n, m;
cin >> n >> m;
vector<int> dp(n + 1, 0);
dp[0] = 1;
for(int i = 0; i <= n; i ++){
for(int j = 1; j <= m; j ++){
if(i >= j) dp[i] += dp[i - j];
}
}
cout << dp[n];
return 0;
}
(十七)零钱兑换
组合问题 + 完全背包
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// INT_MAX在<limits.h>中
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0; //凑成总额为0所需要的硬币数一定是0
for(int i = 0; i < coins.size(); i ++){
for(int j = coins[i]; j <= amount; j ++){
//如果dp[j - coins[i]]为初始化的值(INT_MAX),那么+1后会溢出,而这里dp[j]肯定是保留原来的值,所以就不要求min了
if(dp[j - coins[i]] < INT_MAX) dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
if(dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
(十七)完全平方数
同上一题,组合问题+完全背包
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for(int i = 0; i <= n; i ++){
// j 的范围控制,j*j <= i
for(int j = 1; j * j <= i; j ++){
if(dp[i - j * j] < INT_MAX) dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
};
(十八)单词拆分
排列问题,先遍历背包,再遍历物品
完全背包,从前向后遍历
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for(int i = 1; i <= s.size(); i ++){//排列问题,先遍历背包
for(auto word: wordDict){//再遍历物品
int j = word.size();
// 必须同时满足这3个条件
// 省略了如果如果不选当前词(s.substr(i - j, j) != word),dp[i] = dp[i]
// substr(开始下标,长度)
if(i - j >= 0 && s.substr(i - j, j) == word && dp[i - j]){
dp[i] = dp[i - j];
}
}
}
return dp[s.size()];
}
};
(十九)多重背包
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
法一:每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了(超时)
for (int i = 0; i < n; i++) {
while (nums[i] > 1) { // 物品数量不是一的,都展开
weight.push_back(weight[i]);
value.push_back(value[i]);
nums[i]--;
}
}
法二:把每种商品遍历的个数放在01背包里面在遍历一遍。
#include <iostream>
using namespace std;
#include <vector>
int main(){
int c, n;
cin >> c >> n;
vector<int> weight(n + 1, 0);
vector<int> value(n + 1, 0);
vector<int> num(n + 1, 0);
for(int i = 0; i < n; i ++) cin >> weight[i];
for(int i = 0; i < n; i ++) cin >> value[i];
for(int i = 0; i < n; i ++) cin >> num[i];
vector<int> dp(c + 1, 0);
for(int i = 0; i < n; i ++){//遍历物品
for(int j = c; j >= weight[i]; j --){//遍历背包
// 遍历数量
for(int k = 1; k <= num[i] && j >= k*weight[i]; k ++){
dp[j] = max(dp[j], dp[j - k*weight[i]] + k*value[i]);
}
}
}
cout << dp[c];
return 0;
}
(二十)打家劫舍
- 确定dp数组及其下标的含义
dp[i] : 考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
- 确定递推公式
偷第 i 号:dp[i - 2] +nums[i]
不偷第 i 号:dp[i - 1]
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
- 初始化
dp[0] = nums[0]
dp[1] = max(dp[1], dp[0])
- 遍历顺序
从前往后
- 打印dp数组
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
vector<int> dp(nums.size() + 1, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < nums.size(); i ++){
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
(二十一)打家劫舍II
情况一:包含首元素,不包含尾元素
情况二:不包含首元素,包含尾元素
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
if(nums.size() == 2) return max(nums[0], nums[1]);
// 考虑首元素
int left = robRange(nums, 0, nums.size() - 2);
// 考虑尾元素
int right = robRange(nums, 1, nums.size() - 1);
return max(left, right);
}
int robRange(vector<int>& nums, int startIndex, int endIndex){
vector<int> dp(nums.size(), 0);
// 同下标,方便处理
dp[startIndex] = nums[startIndex];
dp[startIndex + 1] = max(nums[startIndex], nums[startIndex + 1]);
for(int i = startIndex + 2; i <= endIndex; i ++){
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[endIndex];
}
};
(二十二)打家劫舍III
遍历顺序:后序遍历(通过递归函数的返回值做下一步计算)
讨论当前节点抢/不抢
树形dp
(1)暴力解法(超时)
class Solution {
public:
int rob(TreeNode* root) {
if(root == NULL) return 0;
if(!root -> left && !root -> right) return root -> val;
// 偷该节点
int val1 = root -> val;
if(root -> left) val1 += rob(root -> left -> left) + rob(root -> left -> right);
if(root -> right) val1 += rob(root -> right -> left) + rob(root -> right -> right);
// 不偷该节点
int val2 = rob(root -> left) + rob(root -> right);
return max(val1, val2);
}
};
超时的原因:多次重复计算(递归)
(2)优化
使用Map把计算过的节点的结果保存一下,每次判断一下这个节点的Map里面是否有值,如果有值就不需要计算了(通过)
class Solution {
public:
//这个定义在外面,为全局变量,不能定义在递归里面
unordered_map<TreeNode*, int> map;
int rob(TreeNode* root) {
if(root == NULL) return 0;
if(!root -> left && !root -> right) return root -> val;
// 先判断这个节点是否被计算过
if(map[root]) return map[root];
// 偷该节点
int val1 = root -> val;
if(root -> left) val1 += rob(root -> left -> left) + rob(root -> left -> right);
if(root -> right) val1 += rob(root -> right -> left) + rob(root -> right -> right);
// 不偷该节点
int val2 = rob(root -> left) + rob(root -> right);
map[root] = max(val1, val2);
return max(val1, val2);
}
};
(3)动态规划
用dp数组将上一步的状态存储下来
每一步只要存储两种状态,偷/不偷时最大获得金钱
dp[0] :不偷,最大金钱 dp[1]: 偷,最大金钱
由于递归每次回返回上一次的递归结果,所以只要维护一个dp[][]即可
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
vector<int> robTree(TreeNode* cur){
if(cur == NULL) return vector<int>{0,0};
// 左子树的情况
vector<int> left = robTree(cur -> left);
// 右子树的情况
vector<int> right = robTree(cur -> right);
// 偷cur,则不能偷左右子树
int val1 = cur -> val + left[0] + right[0];
// 不偷cur,则可以偷左右子树,也可以不偷左右子树,取最大值
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
//注意不偷和偷的结果一一对应
return {val2, val1};
}
};
(二十三)买卖股票的最佳时机
(1)贪心
class Solution {
public:
int maxProfit(vector<int>& prices) {
int low = INT_MAX;
int result = 0;
for(int i = 0; i < prices.size(); i ++){
// low和result既动态变化,又保留了之前的最值
low = min(result, low);
result = max(result, prices[i] - low);
}
return result;
}
};
(2)动态规划
第 i 天有两种状态:持有这支股票(在 i 或之前将这支股票买下了)的最大利润 dp[i][0],不持有该只股票(在 i 或之前将这支股票卖掉了)的最大利润dp[i][1]
dp[i][0]:
-
如果第 i 天之前就已经买下了该股票,则dp[i][0] = dp[i - 1][0]
-
如果正好第 i 天买入该股票,由于先买才能卖,所以前面积累的利润为0,dp[i][0] = - price[i]
dp[i][0] = max(dp[i -1][0], -price[i])
dp[i][1]:
-
如果第i天之前就不持有该股票,则dp[i][1] = dp[i - 1][1],
-
如果第i天正好卖掉该股票,则dp[i][1] = dp[i - 1][0] + price[i]
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + price[i])
遍历顺序:从前向后 dp[0][0] = -price[0] dp[0][1] = 0
最终结果为dp[len - 1][1],因为最后要得到最大收益,股票一定卖掉了
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len + 1, vector<int>(2, 0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < len; i ++){
dp[i][0] = max(dp[i - 1][0], - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
(二十四)买卖股票的最佳时机II
(1)贪心(见贪心算法一章)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
for(int i = 1; i < prices.size(); i ++){
result += max(prices[i] - prices[i - 1], 0);
}
return result;
}
};
(2)动态规划
区别:可以多次买卖股票,所以如果第 i 天卖入股票,则所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
如果只能买卖一次股票,则为 - prices[i]
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len + 1, vector<int>(2, 0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < len; i ++){
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
(二十五)买卖股票的最佳时机III
最多买卖两次
一天共4个状态
0:第一次持有该股票
1:第一次不持有该股票
2:第二次持有该股票
3:第二次不持有该股票
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len + 1, vector<int>(5, 0));
dp[0][0] = -prices[0];
dp[0][2] = -prices[0];// 这里相当于第一次买卖完后,又买了第二次
for(int i = 1; i < len; i ++){
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i][0] + prices[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
}
return max(max(dp[len - 1][1], dp[len - 1][3]), 0);
}
};
(二十六)买卖股票的最佳时机IV
-
0 表示不操作
-
1 第一次持有该股票
-
2 第一次不持有该股票
-
3 第二次持有该股票
-
4 第二次不持有该股票
-
…
-
2*k - 1 第 k 次持有该股票
-
2*k 第 k 次不持有该股票
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len + 1,vector<int>(2*k + 1, 0));
// 对dp[0][k]进行初始化
dp[0][0] = 0;
// 可以进行k笔交易,因此有2*k种状态(持有/不持有)
for(int i = 1; i <= 2 * k; i += 2){
dp[0][i] = -prices[0];
}
for(int i = 1; i < len; i ++){
dp[i][0] = dp[i - 1][0];
// 一共有2*k种状态
for(int j = 1; j <= 2 * k; j +=2) {
// 第i - 1日也是第i日的状态或者是第i日的前一个状态,再加上/减去第i日的股票价格(第i日卖出/卖入)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
}
}
return dp[len - 1][2*k];//最大利润一定是买卖k次后达到
}
};
(二十七)最佳买卖股票时机含冷冻期
-
状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
-
不持有股票状态,这里就有两种卖出股票状态
-
状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
-
状态三:今天卖出股票
-
-
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(4, 0));
dp[0][0] = -prices[0];
dp[0][1] = 0; // 不持有该股票状态(但不是今天卖出)
dp[0][2] = 0; // 卖出股票状态(今天卖出)
dp[0][3] = 0; // 冷冻状态
for(int i = 1; i < len; i ++){
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][1], dp[i - 1][3]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);//这里存疑,为什么不能由上一天的卖出股票状态得到
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];
}
return max(dp[len - 1][1], max(dp[len - 1][2], dp[len - 1][3]));
}
};
(二十八)买卖股票的最佳时机
同买卖股票的最佳时机II,加上手续费
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int len = prices.size();
vector<vector<int>> dp(len + 1, vector<int>(2, 0));
dp[0][0] = -prices[0];
for(int i = 1; i < len; i ++){
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
// 这里卖出为一次完整交易,支付手续费
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
}
return dp[len - 1][1];
}
};
(二十九)最长递增子序列
- dp[i]的定义
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
- 状态转移方程
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
- dp[i]的初始化
每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1.
- 确定遍历顺序
dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() == 1) return 1;
int result = 0;
// 注意这里的初始化为1,因为对于每个i,对应的最长递增子序列都最少为1
vector<int> dp(nums.size() + 1, 1);
dp[0] = 1;
for(int i = 1; i < nums.size(); i ++){
for(int j = 0; j < i; j ++){
// 注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。
if(nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
// 这里要保存dp[i]最大的结果,不一定是最终的i对应的最长子序列
if(dp[i] > result) result = dp[i];
}
// 这里不是返回dp[nums.size() - 1]!!
return result;
}
};
(三十)最长连续递增序列
连续子序列,只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。
dp[i] = dp[i - 1] + 1;
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
if(nums.size() == 1) return 1;
int result = 1;
vector<int> dp(nums.size() + 1, 1);
for(int i = 1; i < nums.size(); i ++){
if(nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
if(dp[i] > result) result = dp[i];
}
return result;
}
};
(三十一)最长重复子数组
子数组:连续子序列(当前状态由前一个状态推出)
dp[i][j] : 在nums1中以 i 为结尾, nums2中以 j 为结尾的公共子序列长度
if(nums1[i] == nums2[j]) dp[i][j] = dp[i - 1][j - 1] + 1
初始化:i = 0/j = 0 → dp[i][j] = 1
取其中最大的dp[i][j]
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
for(int i = 0; i < nums1.size(); i ++){
for(int j = 0; j < nums2.size(); j ++){
if(nums1[i] == nums2[j]){
// 对于i为0或j为0情况,单独处理
if(i * j == 0){
dp[i][j] = 1;
}else{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
}
// 记录dp[i][j]中出现的最大值
if(dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
(三十二)最长公共子序列
最长公共子序列:不一定连续
dp[i][j]: 长度为 i 的字符串和长度为 j 的字符串的最长公共子序列长度
如果 nums1[i - 1] = nums2[j - 1],则dp[i][j] = dp[i - 1][j - 1]
否则 dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
// 这里i和j表示长度,[1, text1.size()]
for(int i = 1; i <= text1.size(); i ++){
for(int j = 1; j <= text2.size(); j ++){
// 注意这里长度为i但是对应着数组里面的第i - 1个元素,j同理
if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
}
}
// 返回的是这个两个数组对应的最长公共子序列
return dp[text1.size()][text2.size()];
}
};
(三十三)不相交的线
同上,最长公共子序列
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
for(int i = 1; i <= nums1.size(); i ++){
for(int j = 1; j <= nums2.size(); j ++){
if(nums1[i - 1] == nums2[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[nums1.size()][nums2.size()];
}
};
(三十四)最大子序列
dp[i]: 以num[i]结尾的最大连续子序列和为dp[i]
两种选择推出:
-
dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
-
nums[i],即:从头开始计算当前连续子序列和
dp[i] = max(dp[i - 1] + nums[i], nums[i])
注意:最大结果可能在中间某个i处取到,因此需要保存最大结果
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 1. 贪心
// int result = nums[0];
// int count = 0;
// for(int i = 0; i < nums.size(); i++){
// count += nums[i];
// if(count > result){
// // 注意这里要先给result赋值,针对数组大小为1,且其元素小于0的情况
// result = count; //记录当前最好结果
// }
// if(count <= 0){
// count = 0; //当count小于0,对后面为负影响
// }
// }
// return result;
// 2.动态规划
vector<int> dp(nums.size() + 1);
// result 记录中途出现的最大结果,初始化为nums[0]
int result = nums[0];
dp[0] = nums[0];
for(int i = 1; i < nums.size(); i ++){
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
if(dp[i] > result) result = dp[i];
}
return result;
}
};
(三十五)判断子序列
(1)双指针
class Solution {
public:
bool isSubsequence(string s, string t) {
if(s.size() == 0) return true;
int index = 0;
for(int i = 0; i < t.size(); i ++){
if(t[i] == s[index]){
index ++;
if(index == s.size()) return true;
}
}
return false;
}
};
(2)动态规划
转为判断两个数组的公共子数组
dp[i][j]:长度为 i 字符串s,长度为 j 的字符串 t,相同序列的长度为dp[i][j]
①当s[i - 1] = t[j - 1]: dp[i][j] = dp[i - 1][j - 1] + 1
②当s[i - 1] ≠ t[j - 1]:dp[i][j] = dp[i][j - 1]
class Solution {
public:
bool isSubsequence(string s, string t) {
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for(int i = 1; i <= s.size(); i ++){
for(int j = 1; j <= t.size(); j ++){
if(s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];
}
}
return (dp[s.size()][t.size()] == s.size());
}
};
(三十六)不同的子序列
dp[i][j]:长度为 i 的 s 数组中长度为 j 的 t 数组最多出现的个数
①s[i - 1] = t[j - 1]
dp[i][j] = dp[i - 1][j - 1] 或者 i 这一位不取 dp[i][j] = dp[i - 1][j]
dp[i][j] = dp[i - 1][j - 1] + dp[i -1][j]
②s[i - 1] = t[j - 1]
dp[i][j] = dp[i - 1][j]
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1, 0));
for(int i = 0; i <= s.size(); i ++) dp[i][0] = 1;
for(int j = 1; j <= t.size(); j ++) dp[0][j] = 0;
for(int i = 1; i <= s.size(); i ++){
for(int j = 1; j <= t.size(); j ++){
if(s[i - 1] == t[j - 1]){
// 这里可能会超过int返回,使用uint64_t可以通过
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
if(dp[s.size()][t.size()]>INT_MAX) return -1;
return dp[s.size()][t.size()];
}
};
(三十七)两个字符串的删除操作
1)转换为求最长公共子序列。
两个字符串长度减去二倍的公共子序列长度
class Solution {
public:
int minDistance(string word1, string word2) {
// 求两个数组的最长公共序列
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
for(int i = 1; i <= word1.size(); i ++){
for(int j = 1; j <= word2.size(); j ++){
if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return word1.size() + word2.size() - 2* dp[word1.size()][word2.size()];
}
};
2)直接求解
dp[i][j]:长度分别为 i 和 j 的数组nums1和nums2, 最少需要删除dp[i][j],使得两者相同
注意长度为 i ,对应的数组下标为 i - 1
如果nums1[i - 1] = nums2[i - 1],则删除的个数不变, dp[i][j] = dp[i - 1][j - 1]
如果nums1[i - 1] ≠ nums2[i - 1],则考虑word1删掉一个,或者word2删掉一个,取两者最小的,
即dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)
class Solution {
public:
int minDistance(string word1, string word2) {
// 求两个数组的最长公共序列
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
// 如果word1的长度为0,那么要删除的长度为word2的长度
for(int j = 0; j <= word2.size(); j ++) dp[0][j] = j;
// 如果word2的长度为0,那么要删除的长度为word1的长度
for(int i = 0; i <= word1.size(); i ++) dp[i][0] = i;
for(int i = 1; i <= word1.size(); i ++){
for(int j = 1; j <= word2.size(); j ++){
if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
// 如果不匹配,考虑word1删掉一个,或者word2删掉一个
else dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
return dp[word1.size()][word2.size()];
}
};
(三十八)编辑距离
dp[i][j]: 长度为 i 和 j 的word1和word2,将word1转为word2,所需要的最少操作
如果nums1[i - 1] == nums2[j - 1],则不需要操作,dp[i][j] = dp[i - 1][j - 1]
如果nums1[i - 1] ≠ nums2[j - 1],
-
插入一个字符 dp[i][j] = dp[i][j - 1] + 1
-
删除一个字符 dp[i][j] = dp[i - 1][j] + 1
-
替换一个字符 dp[i][j] = dp[i - 1][j - 1] + 1
取其中替换最少的,dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j - 1], dp[i][j - 1])) + 1
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1));
for(int i = 0; i <= word1.size(); i ++) dp[i][0] = i;
for(int j = 0; j <= word2.size(); j ++) dp[0][j] = j;
for(int i = 1; i <= word1.size(); i ++){
for(int j = 1; j <= word2.size(); j ++){
if(word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j - 1], dp[i][j - 1])) + 1;
}
}
return dp[word1.size()][word2.size()];
}
};
(三十九)回文子串
- 确定dp数组(dp table)以及下标的含义
判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。
- 确定递推公式
当s[i]与s[j]不相等,dp[i][j]一定是false。
当s[i]与s[j]相等时,有如下三种情况:
-
情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
-
情况二:下标i 与 j相差为1,例如aa,也是回文子串
-
情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
- dp数组初始化
初始化为false
- 确定遍历顺序
由于dp[i][j]需要根据dp[i + 1][j - 1] 确定,所以 i 为从后向前遍历,j 为从前向后遍历
又因为设定的 j 在 i 的后面构成子字符串,所以 j 从当前 i 开始向后遍历
- 打印dp数组
class Solution {
public:
int countSubstrings(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0;
for(int i = s.size() - 1; i >= 0; i --){
for(int j = i; j < s.size(); j ++){
// 如果s[i] != s[j],dp[i][j]肯定为false
// 如果s[i] = s[j],分三种情况讨论(i和j之间的距离)
if(s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])){
dp[i][j] = true;
// 记录结果
result ++;
}
}
}
return result;
}
};
(四十)最长回文子序列
回文子串要求连续,而回文子序列不要求连续
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;(加上首尾两个相同的字符)
如果s[i]与s[j]不相同,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列
即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for(int i = 0; i < s.size(); i ++) dp[i][i] = 1;
for(int i = s.size() - 1; i >= 0; i --){
for(int j = i + 1; j < s.size(); j ++){
if(s[i] == s[j]) 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][s.size() - 1];
}
};