对于动态规划问题:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
动态规划
基础题目
509. 斐波那契数
70. 爬楼梯
使用递归,时间复杂度2的n次方
使用动态规划时间复杂度n
空间复杂度O(n)
class Solution {
public:
int climbStairs(int n) {
if(n <= 2) return n;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
746. 使用最小花费爬楼梯
dp数组初始化错误,没看到可以从0或者1开始
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i <= cost.size(); i++){
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
};
62. 不同路径
这里初始化下标错误,主要是理解错误dp[0][j]代表第一行,因此dp[m-1][n-1]代表m行n列的路径方法;
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];
}
};
这里添加了障碍物矩阵,我只考虑到遇到障碍物那么路径条数为0;
没考虑到初始化,只有一条路时遇到障碍物之后的路径数为0,写成了遇到障碍物那么路径为0.
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size(), n = obstacleGrid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i = 0; i < m && obstacleGrid[i][0] == 0; i++){
dp[i][0] = 1;
}
for(int j = 0; j < n && obstacleGrid[0][j] == 0; j++){
dp[0][j] = 1;
}
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(obstacleGrid[i][j] == 1){
dp[i][j] = 0;
}
else{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
};
343. 整数拆分
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
这里递归时,只考虑到继续分解dp【i-k】,没有考虑到不分解;
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1, 1);
for(int i = 3; i <= n; i++){
for(int k = 1; k <= i/2; k++){
dp[i] = max(dp[i], max(dp[i - k] * k, (i - k) * k));
}
}
return dp[n];
}
};
96. 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
对n个节点可以拆分成和为n的两个数的二叉搜索树之积;
这里遍历和的时候,从 0开始但是j=1因为dp[j - 1]
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1);
dp[0] = 1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i ;j++){
dp[i] += dp[i - j] * dp[j - 1];
}
}
return dp[n];
}
};
背包问题
问能否能装满背包(最多装多少)
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路是01背包的变形,01背包是在固定容量找最大价值的物品;
本题是找到两个子集元素和相等;因此我们只需找到一个子集元素和为sum/2,也就是01背包问题的价值为sum/2,这里价值和重量都可以看作数组元素;
注意就是遍历方式是从后往前,直到容量nums[i];
class Solution {
public:
bool canPartition(vector<int>& nums) {
//01背包
vector<int> dp(10001,0);
int sum = 0;
for(int i = 0;i < nums.size();i++){
sum += nums[i];
}
if(sum % 2 == 1){
return false;
}
int target = sum / 2;
for(int i = 0; i < nums.size(); i++){
for(int j = target; j >= nums[i]; j--){
dp[j] = max(dp[j],dp[j - nums[i]] + nums[i]);
}
}
if(dp[target] == target){
return true;
}
return false;
}
};
1049. 最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
题意相当于找两个和最相近的子集,那么可以分割相同等和的思路;
容量为sum/2的背包所能装的最大价值(不一定是sum/2)即为和小且相近sum/2;
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(1501,0);
int sum = 0;
for(int i = 0; i < stones.size(); i++){
sum += stones[i];
}
int target = sum / 2;
for(int i = 0; i < stones.size(); i++){
for(int j = target; j >= stones[i]; j--){
dp[j] = max(dp[j],dp[j - stones[i]]+ stones[i]);
}
}
return sum - dp[target] - dp[target];
}
};
问装满背包有几种方法
494. 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
这里转化成01背包问题,根据方程求解+的和subsum满足题解为sum+target/2; 因此数组中subsum组合的个数为答案
推导公式 dp[j] += dp[j - nums[i]];很好理解,加入nums【i】和为j的组合个数为减去nums[i]的组合个数;
初始化注意dp【0】=1,可以举例加入3,此时和为3,那么dp[j - nums[i]]=d[0],就有一种方法;
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(sum < abs(target)){
return 0;
}
if((sum + target) % 2 == 1){
return 0;
}
int subsum = (sum + target) / 2;
vector<int> dp(subsum + 1, 0);
dp[0] = 1;
for(int i = 0; i < nums.size(); i++){
for(int j = subsum; j >= nums[i]; j--){
dp[j] += dp[j - nums[i]];
}
}
return dp[subsum];
}
};
518. 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
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];
}
};
377. 组合总和 Ⅳ(排列)
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1,0);
dp[0] = 1;
for(int i = 1; i <= target; i++){
for(int j = 0; j < nums.size(); j++){
if(nums[j] <= i && dp[i - nums[j]] < INT_MAX - dp[i]){
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
总结
零钱兑换和组合总数题目类似,零钱兑换是无序是组合问题,组合总和是有序是排序问题;从完全背包的解法,组合问题需要先遍历物品再遍历背包,这样外层循环,物品i+1一定在物品i之后,排序问题需要先遍历背包容量,再遍历物品。
问装满背包所有物品的最小个数
dp[j] = min(dp[j - coins[i]] + 1, dp[j])
322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
因为这里求解最少硬币的个数,因此数组初值应该为INT最大值,dp[0]应该等于0;
最少个数,递归公式 dp[j] = min(dp[j], dp[j - coins[i]] + 1);
当初值没变时代表没有组合满足条件时返回-1
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//组合问题
if(amount == 0) return 0;
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for(int i = 0; i < coins.size(); i++){
for(int j = coins[i]; j <= amount; j++){
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];
}
};
279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
理清楚谁是背包谁是物品就可以;
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1,INT_MAX);
dp[0] = 0;
//n是容量,平方数是物品
for(int i = 1; i * i <= n; i++){
for(int j = i * i; j <= n; j++){
dp[j] = min(dp[j - i * i] + 1,dp[j]);
}
}
return dp[n];
}
};
问背包装满最大价值
:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
背包问题遍历的时候从后往前,从最大容量到当前容量;
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for(auto& str : strs){
int zero = 0,one = 0;
for(auto& c : str){
if(c == '0'){
zero++;
}
else{
one++;
}
}
for(int i = m; i >= zero; i--){
for(int j = n; j >= one; j--){
dp[i][j] = max(dp[i][j],dp[i - zero][j - one] + 1);
}
}
}
return dp[m][n];
}
};
139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
单词拆分问题,背包容量是s长度,物品是单词
本题不是简单的计数,每次遍历都需要判断从s中截取的子串是否在单词数组内,如果在且这个子串尾巴上也true
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(),wordDict.end());
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
//背包
for(int i = 1; i <= s.size(); i++){
//物品
for(int j = 0; j < i; j++){
string word = s.substr(j, i - j);
if(wordSet.find(word) != wordSet.end() && dp[j]){
dp[i] = true;
}
}
}
return dp[s.size()];
}
};
打家劫舍系列
198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
初始化dp[0],dp[1]要注意,还有判断数组数量等于1时判断
class Solution {
public:
int rob(vector<int>& nums) {
//最大容量
int n = nums.size();
if(n == 1){
return nums[0];
}
vector<int> dp(n, 0);
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i = 2; i < n; i++){
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
};
213.打家劫舍II
考虑首尾不可同时取,分两种情况,一个只取首不取尾,一个只取尾不取头;之前我考虑如果取最后的时候再减去首的数字,但这是错误的,因为你不知道前面dp值是否加了首的数字
class Solution {
public:
int getValue(vector<int>& nums, int start, int end){
vector<int> dp(nums.size(), 0);
dp[start] = nums[start];
dp[start + 1] = max(nums[start],nums[start + 1]);
for(int i = start + 2; i <= end; i++){
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[end];
}
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1){
return nums[0];
}
if(n == 2){
return max(nums[0],nums[1]);
}
int res1 = getValue(nums, 0 , nums.size() - 2);
int res2 = getValue(nums, 1, nums.size() - 1);
return max(res1, res2);
}
};
337.打家劫舍III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
二叉树加上动态规划;这里使用后序遍历,二维数组0代表不偷,1代表偷,递归遍历左右子树;
class Solution {
public:
int rob(TreeNode* root) {
vector<int> ans = robTree(root);
return max(ans[1],ans[0]);
}
vector<int> robTree(TreeNode* cur){
if(cur == nullptr) return vector<int>{0,0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
int val1 = cur->val + left[0] + right[0];
int val2 = max(left[0],left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
买卖股票系列
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
这里可以遍历查找最低价格最高价格即可,
也可使用动态规划,0表示持有i股票的最大金额,1表示不持有股票最大金额,最后不持有的一定比持有金额多
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n == 0) return 0;
vector<vector<int>> dp(n, vector<int>(2,0));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < n; 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[n - 1][1];
}
};
122.买卖股票的最佳时机II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
和上个题不同点在于可以无限次购买出售,故每次购买出售的金额都与之前的交易金额有关
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2, 0));
dp[0][0] = -prices[0];
for(int i = 1; i < n; 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[n - 1][1];
}
};
123. 买卖股票的最佳时机 III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
题目要求只能进行两次交易,因此给定五个状态:不购买,第一次持有,第一次卖出,第二次持有,第二次卖出;
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(5,0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for(int i = 1; i < n; i++){
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i -1][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]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[n - 1][4];
}
};
188. 买卖股票的最佳时机 IV
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
使用2k+1个数记录对每个物品每个交易的状态;
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
int stateCode = 2 * k + 1;
vector<vector<int>> dp(n,vector<int>(stateCode,0));
for(int i =1; i < stateCode; i += 2){
dp[0][i] = -prices[0];
}
for(int i = 1; i < n; i++){
for(int j = 1; j < stateCode; j += 2){
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[n - 1][stateCode - 1];
}
};
309.最佳买卖股票时机含冷冻期
加入冷冻期以后会有四个状态:
0:持有一只股票;1:不持有;2:今天卖出:3:冷冻期主要是状态转移要理清楚
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(4,0));
dp[0][0] = -prices[0];
for(int i = 1; i < n; i++){
dp[i][0] = max(dp[i - 1][0],max(dp[i - 1][1] - prices[i],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[n - 1][1],max(dp[n - 1][2],dp[n - 1][3]));
}
};
714. 买卖股票的最佳时机含手续费
含手续费,在每次状态更改为卖出时获得价格还需减去手续费
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2, 0));
dp[0][0] = -prices[0];
for(int i = 1; i < n; 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[n - 1][1];
}
};
子序列系列
300. 最长递增子序列
dp[i]含义就是以nums[i结尾的最长子序列;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1);
int ans = 1;
for(int i = 1; i < n; i++){
for(int j = 0; j < i ; j++){
if(nums[i] > nums[j]){
dp[i] = max(dp[i], dp[j] + 1);
}
if(dp[i] > ans){
ans = dp[i];
}
}
}
return ans;
}
};
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
连续递增序列因此只需要比较每个值和前一个值
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1);
int ans = 1;
for(int i = 1; i < n; i++){
if(nums[i] > nums[i - 1]){
dp[i] = dp[i - 1] + 1;
}
if(dp[i] > ans){
ans = dp[i];
}
}
return ans;
}
};
718. 最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
dp代表以i-1为尾,j-1尾的最长重复子数组;来减少初始化
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 ans = 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;
}
if(dp[i][j] > ans) ans = dp[i][j];
}
}
return ans;
}
};
1143. 最长公共子序列、1035. 不相交的线
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
最长重复子数组不同的是,子序列可以是间断的,因此转移状态分两种,一个是字母相同,一个是字母不同
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1 = text1.size(), n2 = text2.size();
vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));
for(int i = 1; i <= n1; i++){
for(int j = 1; j <= n2; j++){
if(text1[i - 1] == text2[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[n1][n2];
}
};
392.判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
最长公共子序列思路,只是需要最后判断公共序列数量是不是s的数量
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] = max(dp[i - 1][j],dp[i][j - 1]);
}
}
}
if(dp[s.size()][t.size()] == s.size()){
return true;
}
return false;
}
};
##115. 不同的子序列
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
题目数据保证答案符合 32 位带符号整数范围。
dp含义是以i-1结尾的s子序列中出现以j-1结尾的t的个数;
数据范围溢出unsigned long long
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<unsigned long long>> dp(s.size() + 1, vector<unsigned long long>(t.size() + 1, 0));
for(int i = 0; i <= s.size(); i++) dp[i][0] = 1;
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] + dp[i - 1][j];
}
else{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[s.size()][t.size()];
}
};
583. 两个字符串的删除操作
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
输入: “sea”, “eat”
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
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] + 1, dp[i][j - 1] + 1);
}
}
}
return dp[word1.size()][word2.size()];
}
};
72. 编辑距离
插入word1一个字符相当于删除word2一个字符
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 = 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{
//插入(相当于2删除),替换,删除
dp[i][j] = min({dp[i][j - 1],dp[i - 1][j],dp[i - 1][j - 1]}) + 1;
}
}
}
return dp[word1.size()][word2.size()];
}
};
516. 最长回文子序列
代表从i到j的字符串中最长子序列长度
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = 0;i < n; i++) dp[i][i] = 1;
for(int i = n - 1; i >=0; i--){
for(int j = i + 1 ; j < n; 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][n - 1];
}
};