【代码随想录】二刷-动态规划
动态规划
解题步骤:
- 确定dp数组
- 确定递推公式——递推公式决定dp数组要如何初始化
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
509. 斐波那契数
class Solution {
public:
int fib(int n) {
if(n <= 1)return n;
// 推导公式: dp[n] = dp[n-1]+dp[n-2]
int dp[2];
// 初始化
dp[0] = 0;
dp[1] = 1;
// 从前往后
for(int i = 2; i <= n;i++){
int tmp = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = tmp;
}
return dp[1];
}
};
70. 爬楼梯
class Solution {
public:
int climbStairs(int n) {
if(n <= 2)return n;
// dp[n] = d[n-1] + dp[n-2]
int dp[2];
dp[0] = 1;// 1阶楼梯-1种方法
dp[1] = 2;// 2阶楼梯-2种方法
for(int i = 3; i <= n;i++){
int tmp = dp[1] + dp[0];
dp[0] = dp[1];
dp[1] = tmp;
}
return dp[1];
}
};
746. 使用最小花费爬楼梯
// 时间复杂度O(n)
// 空间复杂度O(n)
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
// 到达第i阶台阶的最少花费
vector<int>dp(cost.size()+1,0);
// 可以选择从0阶或1阶开始爬,所以初始化0 1阶都为0
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[cost.size()];
}
};
62. 不同路径
- 二维数组
// 时间复杂度O(mxn)
// 空间复杂度O(mxn)
class Solution {
public:
int uniquePaths(int m, int n) {
// dp[i][j]: 到达i j点,有多少种方法
vector<vector<int>>dp(m,vector<int>(n,0));
// 初始化-第一行只有一条路径,注: 小叮当每次只能向下或者向上进行移动
for(int i = 0; i < n;i++)dp[0][i] = 1;
// 初始化-第一列只有一条路径
for(int i = 0; i < m;i++)dp[i][0] = 1;
// 遍历顺序,从1,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];
}
};
- 滚动数组
// 时间复杂度O(mxn)
// 空间复杂度O(m)
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int>dp(n,0);
for(int i = 0; i< n;i++)dp[i] = 1;
// 相当于每行开始就已经把上放的加到自己上面了,遍历的时候只需要将左边的加上即可
for(int i = 1; i < m;i++){
for(int j = 1; j < n;j++){
dp[j] += dp[j-1];
}
}
return dp[n-1];
}
};
63. 不同路径 II
// 时间复杂度O(mxn)
// 空间复杂度O(mxn)
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& ob) {
int m = ob.size();//行
int n = ob[0].size();// 列
// 如果起点 or 终点出现障碍,直接返回
if(ob[0][0] == 1 ||ob[m-1][n-1] == 1)return 0;
// dp[i][j]: 到i j点有多少种路径
vector<vector<int>>dp(m,vector<int>(n,0));
// 初始化同上一题,但是注意,左边为障碍物,后面就都是0了
for(int i = 0; i < n && ob[0][i] == 0;i++)dp[0][i] = 1;
// 初始化-同上说明
for(int i = 0; i < m && ob[i][0] == 0;i++)dp[i][0] = 1;
// 遍历顺序: 从1,1 点出发
for(int i = 1; i < m;i++){
for(int j = 1;j < n;j++){
if(ob[i][j] == 1)continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
- 滚动数组
// 时间复杂度O(mxn)
// 空间复杂度O(m)
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& ob) {
if(ob[0][0] == 1 || ob[ob.size()-1][ob[0].size()-1] == 1)return 0;
vector<int>dp(ob[0].size(),0);
// 初始化第一行
for(int i = 0; i < dp.size();i++){
if(ob[0][i] == 1)dp[i] = 0;// 障碍
else if(i == 0)dp[i] = 1;
else dp[i] = dp[i-1];// 过不来了
}
for(int i = 1;i < ob.size();i++){
for(int j = 0; j < dp.size();j++){
if(ob[i][j] == 1)dp[j] = 0;// 障碍
else if(j != 0)dp[j]+= dp[j-1];// !=0 防止越界
// 相当于每行开始就已经把上放的加到自己上面了,遍历的时候只需要将左边的加上即可
}
}
return dp[dp.size()-1];
}
};
343. 整数拆分
// 时间复杂度O(n^2)
// 空间复杂度O(n)
class Solution {
public:
int integerBreak(int n) {
// dp[i]: 数字i可以得到的最大乘积
vector<int>dp(n+1);
// 初始化
dp[2] = 1;
// 遍历顺序
for(int i =3; i <= n;i++){
for(int j = 1; j < i-1;j++){// 可以优化为j <= i /2 因为拆分成最大,应该是两个近似相同的数
// 多次计算,所以要和dp[i]比较
dp[i] = max(dp[i],max((i-j)*j,dp[i-j]*j));// 递归公式,当前两个数 or 接着拆分取最大乘
}
}
return dp[n];
}
};
96. 不同的二叉搜索树
// 时间复杂度: O(n^2)
// 空间复杂度: O(n)
class Solution {
public:
int numTrees(int n) {
// dp[i]: 用值为1-i的i个节点组成的二叉搜索树的个数为dp[i]
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[j-1]*dp[i-j];// 递推公式
}
}
return dp[n];
}
};
背包问题
01背包
前言
- 什么是01背包?
- 有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i]。
- 每个件物品只能用一次,求解将哪些物品装入背包里的物品总价值最大。
- 即,每件物品只有选或不选两种情况
- 二维dp数组01背包
- dp[i][j]: 表示从下标为0-i的物品里任意取,放进容量为j的背包,价值总和最大是多少。从两个方向推出来:
- 不放物品i: 由dp[i-1][j]推出,即不放当前物品i,最大价值就是前面判断出来的。——即当前容量不够i的了,最大值还是之前的。
- 放物品i: 由dp[i-1][j-weight[i]]推出,即先求出不放物品i可以达到的最大值,完了再加上物品i的价值,求得放物品i可以达到的最大值。
- dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- 如何初始化
- 根据dp数组的定义进行初始化
- dp[0] = 0
- dp[0][j]: 放第0件物品容量为j的最大值,value[0] > j 值为0,反之为value[0]
- 确定遍历顺序
- 先遍历物品or背包都可以,但是先遍历物品更好理解
- 先遍历物品: 尝试将每个物品放到每个背包中,求得背包最大值。
- 先遍历背包: 背包i依次尝试每个物品,求得背包最大值。
- 补充: 动态规划:关于01背包问题,你该了解这些!
- 滚动数组: 压缩空间
- 将上一层的拷贝下来,递推公式变为:
- dp[i][j] = max(dp[i][j],dp[i][j-weight[i]]+value[i])
- 需要满足的条件是上一层可以重复利用
- 推导出使用一维dp数组:
- dp数组含义
- dp[j]: 容量为j的背包,所背物品价值最大可以为dp[j]
- 递推公式
- 通过dp[j-weight[i]]推导,dp[j-weight[i]]+value[i]为不装i可到达的最大价值+物品i的价值,当前背包放入物品i达到的最大价值
- dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);取大的,不放物品i为dp[j],放物品i为后者。
- 初始化
- 注意要与定义相吻合
- dp数组在推导的时候取大值,不能被初始值覆盖,所以初始化为0即可。
- 遍历顺序
- 先正序遍历物品,然后倒序遍历背包
- 倒序遍历背包防止物品被加入多次
- 为什么二维dp就不需要倒序?
- 因为二维dp的值是通过上一层计算而来,不会覆盖本层。
- 注: 不能先遍历背包,这会导致每个背包只会被放入一个物品。
- 以下的题都要转化为背包问题
416. 分割等和子集
// 时间复杂度 O(n^2)
// 空间复杂度 O(n)
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum % 2 == 1)return false;// 整数,所以出现小数分不开
int target = sum/2;// 背包大小
vector<int>dp(target+1,0);
dp[0] = 0;// 初始化,容量为0的背包,可累加最大值为0.
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
// 时间复杂度: O(mxn)
// 空间复杂度: O(m)
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
// 类似于上一题,将石头分为了两堆,用全部重量减去这两堆看还能省多少,就是结果
// 转换了一下
int sum = accumulate(stones.begin(),stones.end(),0);
int target = sum / 2;
vector<int>dp(target+1,0);
dp[0] = 0;
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. 目标和
// 时间复杂度:O(n × m),n为正数个数,m为背包容量
// 空间复杂度:O(m),m为背包容量
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
// 转换为填满x的背包有几种方法
// 表达式加法总和为x,减法总和sum-x
// x-(sum-x)= target
// 即 x = (target+sum)/2
int sum = accumulate(nums.begin(),nums.end(),0);
// 两种没有方案的情况
if(abs(target) > sum)return 0;
if((target + sum) % 2 == 1)return 0;
int tag = (target + sum)/2;
vector<int>dp(tag+1,0);
dp[0] = 1;// 没有实际意义,递推公式推导所得
for(int i = 0; i < nums.size();i++){
for(int j = tag;j >= nums[i];j--){
dp[j] += dp[j-nums[i]];// 组合问题-递推公式
}
}
return dp[tag];
}
};
474. 一和零
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
// dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
for(string& str: strs){
int oneCnt = 0;
int zeroCnt = 0;
for(char& c: str){
if(c == '0')zeroCnt++;
else oneCnt++;
}
// 两个维度都是背包容量,一个是1 一个是0
// 遍历到时候相当于两个滚动数组,后序遍历两个容量
for(int i = m; i >= zeroCnt;i--){
for(int j = n; j >= oneCnt;j--){
// 递推公式
dp[i][j] = max(dp[i][j],dp[i-zeroCnt][j-oneCnt]+1);
}
}
}
return dp[m][n];
}
};
完全背包
前言
- 与01背包不同的是,完全背包中,每种物品有无限件。
- 遍历时,内层的背包容量遍历,要从小到大遍历。
- 尽管在一维dp数组中,完全背包的遍历顺序也是无所谓的,这里指的是先遍历容器还是先遍历物品。
- 依然注意下面问题的转换
518. 零钱兑换 II
class Solution {
public:
int change(int amount, vector<int>& coins) {
// 凑成总金额j的货币组合数为dp[j]
vector<int>dp(amount+1,0);
dp[0] = 1;// 初始化—凑合为0金额,有一种
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];
}
};
- 补充:
- 组合数外层for循环遍历物品,内层遍历背包
- 排列数外层for循环背包,内层遍历物品
377. 组合总和 Ⅳ
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
// 凑成i的排列组合个数为dp[i]
vector<int>dp(target+1,0);
// 初始化,从前一个得出,可以说没有一起
dp[0] = 1;
// 求组合,先遍历背包
for(int i = 0 ;i <= target ;i++){
for(int j = 0; j < nums.size();j++){
// 有个册数用例相加超过intg
if(i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i-nums[j]]){
dp[i] += dp[i-nums[j]];
}
}
}
return dp[target];
}
};
70. 爬楼梯
class Solution {
public:
int climbStairs(int n) {
// 爬i阶台阶,dp[i]种方法
vector<int>dp(n+1,0);
dp[0] = 1;/
for(int i = 1;i <= n ;i++){// 背包
for(int j = 1; j <= 2;j++){// 物品
if(i - j >= 0)dp[i] += dp[i-j];// 求排列
}
}
return dp[n];
}
};
322. 零钱兑换
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 组成金额i,所需要的最少硬币数为dp[i]
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++){// 背包
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. 完全平方数
class Solution {
public:
int numSquares(int n) {
// 和为i的最少完全平方数为dp[i]
vector<int>dp(n+1,INT_MAX);
dp[0] = 0;
// 求个数-不要强调顺序
for(int i = 0;i <= n;i++){// 背包
for(int j = 1; j*j <= i;j++){// 物品
dp[i] = min(dp[i],dp[i-j*j]+1);// 递推公式
}
}
return dp[n];
}
};
139. 单词拆分
// 时间复杂度: O(n^3)
// 空间复杂度: O(n)
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
// 将字典存到哈希表中,便于使用
unordered_set<string>set(wordDict.begin(),wordDict.end());
// dp[i],长度为i的字符串,是否可以由字典组成
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++){// 物品-遍历截取s
string word = s.substr(j,i-j);
// 该字串在字典中,并且组成的前一段子串也在字典中
// 0到j和j到j+i-j,组成这个子串
if(set.find(word) != set.end() && dp[j])dp[i] = true;
}
}
return dp[s.size()];
}
};
打家劫舍问题
198. 打家劫舍
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
if (n == 1) return nums[0];
// dp[i]: 下标i内包括i,可以偷的最高金额是dp[i]
vector<int>dp(n,0);
// 初始化
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
// 遍历
for(int i = 2 ; i < n; i++){
// 两种情况:
// 不偷当前这家,考虑前一家
// 偷当前这家,累加上i-2这家
// 也就是偷或者不偷,偷累加当前与i-2,不偷还取前一家可以去的最大值。
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[n-1];
}
};
213. 打家劫舍 II
class Solution {
public:
// 同上一题,只是增加的范围的讨论
int range(vector<int>& nums,int start,int end){
if(start == end)return nums[start];
vector<int>dp(nums.size());
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-2]+nums[i],dp[i-1]);
}
return dp[end];
}
int rob(vector<int>& nums) {
// 分为三种情况:
// 1 不考虑头尾,
// 2 考虑头,不考虑尾
// 3 考虑尾,不考虑头
// 考虑为有可能会用到,并不是一定要用
// 情况1已经被包括在2 3 中
// 所以值考虑后两种情况即可
int n = nums.size();
if(n == 0)return 0;
if(n == 1)return nums[0];
int ret1 = range(nums,0,n-2);
int ret2 = range(nums,1,n-1);
return max(ret1,ret2);
}
};
337. 打家劫舍 III
// 时间复杂度:O(n),每个节点只遍历了一次
// 空间复杂度:O(log n),算上递推系统栈的空间
class Solution {
public:
// 注意: 0-为偷情况,1-为不偷情况
vector<int>robTree(TreeNode* cur){
if(!cur)return vector<int>{0,0};
// 后序遍历
vector<int>left = robTree(cur->left);
vector<int>right = robTree(cur->right);
// 中-单层递归逻辑
//偷当前结点:当前val+左右不偷情况
int val1 = cur->val + left[0]+right[0];
// 不偷当前结点: 左右取大的去情况相加
int val2 = max(left[0],left[1])+max(right[0],right[1]);
return {val2,val1};
}
int rob(TreeNode* root) {
auto ret = robTree(root);
return max(ret[0],ret[1]);
}
};
- 记忆化递推
// 后序遍历
class Solution {
public:
unordered_map<TreeNode* , int> umap;
int rob(TreeNode* root) {
if(!root)return 0;
if(!root->left && !root->right)return root->val;
if(umap[root])return umap[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);
umap[root] = max(val1,val2);
return max(val1,val2);
}
};
买卖股票问题
121. 买卖股票的最佳时机
// 时间复杂度:O(n)
// 空间复杂度:O(n)
class Solution {
public:
int maxProfit(vector<int>& prices) {
// dp[i][0] 表示第i天不持有股票所得最多现金
// dp[i][1] 表示第i天持有股票所得最多现金
vector<vector<int>>dp(prices.size(),vector<int>(2,0));
dp[0][0] = 0;// 不持有-卖出
dp[0][1] -= prices[0];// 持有-买入
for(int i = 1 ; i < prices.size();i++){
// 不持有当天股票: 保持现状or卖出-之前持有状态+当天股票价格 = 卖出
// 可以理解为,之前所获得的最大利润和当天卖出可获得的利润
// 当天可获得的利润为: 之前找到的最小的买入值,与今天的价格进行求利润
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
// 持有(买入)当天股票: 保持现状-之前买入的状态 or 今天买入,选大的
// 可以理解为找更小的买入
dp[i][1] = max(dp[i-1][1],-prices[i]);
}
return dp[prices.size()-1][0];// 卖出的状态肯定的大
}
};
- 空间优化
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int maxProfit(vector<int>& prices) {
// dp[i][0] 表示第i天不持有股票所得最多现金
// dp[i][1] 表示第i天持有股票所得最多现金
vector<vector<int>>dp(2,vector<int>(2,0));
dp[0][0] = 0;// 不持有-考虑卖出
dp[0][1] -= prices[0];// 持有-考虑买入
for(int i = 1 ; i < prices.size();i++){
// 不持有当天股票: 保持现状or卖出-之前持有状态+当天股票价格 = 卖出
dp[i % 2][0] = max(dp[(i-1)%2][0],dp[(i-1)%2][1]+prices[i]);
// 持有(买入)当天股票: 保持现状-之前买入的状态 or 今天买入,选大的
dp[i%2][1] = max(dp[(i-1)%2][1],-prices[i]);
}
return dp[(prices.size()-1)%2][0];// 卖出的状态肯定的大
}
};
122. 买卖股票的最佳时机 II
// 时间复杂度:O(n)
// 空间复杂度:O(n)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>>dp(n,vector<int>(2,0));
dp[0][0] = 0;// 不持有-卖出
dp[0][1] -= 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][0];
}
};
123. 买卖股票的最佳时机 III
// 时间复杂度:O(n)
// 空间复杂度:O(n × 5)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>>dp(n,vector<int>(5,0));
// 分两次买的情况,买卖或者延续之前的状态
// 五种情况
/*
0 没有操作 (其实我们也可以不设置这个状态)
1 第一次持有股票
2 第一次不持有股票
3 第二次持有股票
4 第二次不持有股票
*/
dp[0][0] = 0;
dp[0][1] -= prices[0];
dp[0][2] = 0;
dp[0][3] -= prices[0];
dp[0][4] = 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
// 上一题的进阶版,根据2*k+1讨论奇偶情况,卖出还是买入
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
vector<vector<int>>dp(n,vector<int>(2*k+1,0));
// 初始化-奇数次的情况-即考虑买入情况
// 注意边界控制
for(int i = 1; i < 2*k+1;i += 2){
dp[0][i] = -prices[0];
}
for(int i = 1;i < n;i++){
// 小于2*k -1防止+2越界
for(int j = 0; j < 2*k-1;j += 2){
// 考虑买入情况
dp[i][j+1] = max(dp[i-1][j+1],dp[i-1][j]-prices[i]);
// 考虑卖出情况,注意+1,在之前的买入状态上再卖出
dp[i][j+2] = max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
}
}
// 最后一考虑卖出状态为最大值
return dp[n-1][2*k];
}
};
309. 最佳买卖股票时机含冷冻期
//时间复杂度:O(n)
//空间复杂度:O(n)
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector>dp(n,vector(4,0));
/*
0- 持有股票状态-考虑买入
- 昨天就是买入状态
- 昨天保持卖出状态,一直还没进行之后的买入
- 昨天冷冻期
1- 保持卖出股票状态
- 保持考虑卖出状态,即买入了在等待卖出
- 昨天的冷冻期
2- 今天卖出
- 取到昨天考虑卖出的情况
3- 今天为冷冻期
- 取作品卖出情况
*/
dp[0][0] -= prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
dp[0][3] = 0;
for(int i = 1; i < n;i++){
// 持有股票: 之前的买入状态, or 选取一个之前大的状态然后买入今天的
dp[i][0] = max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i]);
// 保持之前的卖出状态,卖出状态 or 冷冻期
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][3],max(dp[n-1][2],dp[n-1][1]));
}
};
714. 买卖股票的最佳时机含手续费
// 时间复杂度:O(n)
// 空间复杂度:O(n)
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
// II的升级版,增加手续费,在卖出的时候,防止多次计算
int n = prices.size();
vector<vector<int>>dp(n,vector<int>(2,0));
// 不持有-考虑卖出
dp[0][0] = 0;
// 持有-考虑买入
dp[0][1] -= prices[0];
for(int i = 1 ;i < n;i++){
// 卖出的时候计算手续费,防止手续费多次计算
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return max(dp[n-1][1],dp[n-1][0]);
}
};
子序列问题
300. 最长递增子序列
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
int n = nums.size();
if(n <= 1)return n;
int ret = 0;
vector<int>dp(n,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] > ret)ret = dp[i];// 收集大值
}
return ret;
}
};
674. 最长连续递增序列
// 时间复杂度:O(n)
// 空间复杂度:O(n)
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
if(n <= 1)return n;
// dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]。
int ret = 0;
vector<int>dp(n,1);
for(int i = 1; i < n; i++){
if(nums[i] > nums[i-1]){
dp[i] = dp[i-1]+1;
}
ret = max(ret,dp[i]);
}
return ret;
}
};
718. 最长重复子数组
// 时间复杂度:O(n × m),n 为A长度,m为B长度
// 空间复杂度:O(n × m)
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
// 以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]
int n1 = nums1.size();
int n2 = nums2.size();
int ret = 0;
vector<vector<int>>dp(n1+1,vector(n2+1,0));
// 遍历
for(int i = 1; i <= n1;i++){
for(int j = 1 ;j <= n2;j++){
// 递推
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}
if(dp[i][j] > ret)ret = dp[i][j];
}
}
return ret;
}
};
- 空间优化
//时间复杂度:$O(n × m)$,n 为A长度,m为B长度
//空间复杂度:$O(m)$
class Solution {
public:
// 到i-1,统一处理所有情况
int findLength(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size();
int n2 = nums2.size();
vector<int>dp(n1+1,0);
int ret = 0;
for(int i = 1 ; i <= n1; i++){
for(int j = n2; j > 0; j--){// 反着遍历b避免重复覆盖
if(nums1[i-1] == nums2[j-1]){
dp[j] = dp[j-1]+1;
}else dp[j] = 0;// 注意这里不等时的赋0操作
if(dp[j]> ret)ret = dp[j];
}
}
return ret;
}
};
1143. 最长公共子序列
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
//dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
int n1 = text1.size();
int 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];
}
};
1035. 不相交的线
// 转化题意,数字的相对顺序不改变即可,还是求最长公共序列的长度,可以不连续,同上
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size();
int n2 = nums2.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(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[n1][n2];
}
};
53. 最大子数组和
// 时间复杂度:O(n)
// 空间复杂度:O(n)
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// dp[i]:包括下标i之前的最大连续子序列和为dp[i]。
int n = nums.size();
vector<int>dp(n,0);
dp[0] = nums[0];
int ret = dp[0];// 收集最大结果
// 遍历顺序
// 由两个方向可以推出,之前的加上自己的,或是从自己这里重新开始
for(int i = 1 ;i < n;i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);// 递推公式-从这里两个中收集大的,会改变值,所以要有个额外变量来收集结果
if(dp[i] > ret)ret = dp[i];
}
return ret;
}
};
编辑距离
392. 判断子序列
// 时间复杂度:O(n × m)
// 空间复杂度:O(n × m)
// 同上,不同之处在于,不同时,往回调整t
class Solution {
public:
bool isSubsequence(string s, string t) {
// 问题转化,求最长公共子序列
// 最后看长度是否等于s
int n1 = s.size();
int n2 = t.size();
// dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,最大相同子序列的长度为dp[i][j]。
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(s[i-1] == t[j-1])dp[i][j] = dp[i-1][j-1]+1;
else dp[i][j] = dp[i][j-1];// 注意这里,不同时往回调整t
}
}
return dp[n1][n2] == n1;
}
};
115. 不同的子序列
class Solution {
public:
int numDistinct(string s, string t) {
// 也要求不用连续
// 在上题基础上,不找t与s的最长相等子序列了,找t的子序列在s中出现的个数
int n1 = s.size();
int n2 = t.size();
// dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
vector<vector<uint64_t>>dp(n1+1,vector<uint64_t>(n2+1,0));
// 以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
// 删光s出现1次空字符串,包含啦
for(int i = 0; i < n1;i++)dp[i][0] = 1;
// 空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
// 空s可肯定不包括t中任何子序列
//for(int i = 1; i < n2;i++)dp[0][i] = 0;// 其实dp数组初始化时已经赋值完成
// 遍历顺序
for(int i = 1 ; i <= n1 ;i++){
for(int j = 1; j <= n2; j++){
// 因为要计算t的子序列出现在s中的个数,所以类加上之前的状态dp[i-1][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];// 调整s
}
}
return dp[n1][n2];
}
};
583. 两个字符串的删除操作
class Solution {
public:
int minDistance(string word1, string word2) {
// 以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。
int n1 = word1.size();
int n2 = word2.size();
vector<vector<int>>dp(n1+1,vector<int>(n2+1,0));
// 初始化
// word1删除几次与空字符串相等
for(int i = 0; i <= n1;i++)dp[i][0] = i;
// word2删除几次与空字符串相等
for(int i = 0; i <= n2;i++)dp[0][i] = i;
for(int i = 1; i <= n1;i++){
for(int j = 1; j <= n2;j++){
if(word1[i-1] == word2[j-1]){// 相等,不用增加删除次数
dp[i][j] = dp[i-1][j-1];
// 不等情况,删除word1[i-1],删除word2[j-1],值+1,或都删除,值为上一种情况+2
}else dp[i][j] = min(dp[i-1][j-1]+2,min(dp[i][j-1],dp[i-1][j])+1);
}
}
return dp[n1][n2];
}
};
- 或是同1143,求得两个最长公共前后缀,然后用两个字符串的长度减去即可
class Solution {
public:
int minDistance(string word1, string word2) {
// 以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,最长相等子序列
int n1 = word1.size();
int n2 = word2.size();
vector<vector<int>>dp(n1+1,vector<int>(n2+1,0));
int ret = 0;
for(int i = 1; i <= n1;i++){
for(int j = 1; j <= n2;j++){
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;// 相等长度+1
}else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
// 求得两个最长公共前后缀,然后用两个字符串的长度减去即可,求得要删除的次数
return n1+n2-dp[n1][n2]*2;
}
};
72. 编辑距离
// 添加操作同删除操作,这个增加一个,相当于另一个减少,替换操作,可以想象成,之前的已经都判断相等了,求得使得相等的最少操作次数了,这里不等了,在就替换这个字符,即+1步
class Solution {
public:
int minDistance(string word1, string word2) {
// dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
// 即使得到word[i-1] word2[i-1]两个字符串相等的最少操作数为多少。
int n1 = word1.size();
int n2 = word2.size();
vector<vector<int>>dp(n1+1,vector<int>(n2+1,0));
// 初始化同一题
for(int i = 0; i <= n1;i++)dp[i][0] = i;
for(int i = 0; i <= n2;i++)dp[0][i] = i;
for(int i = 1 ;i <= n1; i++){
for(int j = 1; j <= n2;j++){
if(word1[i-1] == word2[j-1]){// 相等,不需要操作
dp[i][j] = dp[i-1][j-1];
// 一个删除操作,就相当于另一个增加操作
}else dp[i][j] = min(min(dp[i][j-1]+1,dp[i-1][j]+1),dp[i-1][j-1]+1);//1可以提出来
// dp[i][j] = min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]})+1;
}
}
return dp[n1][n2];
}
};
647. 回文子串
// 时间复杂度:O(n^2)
// 空间复杂度:O(n^2)
class Solution {
public:
int countSubstrings(string s) {
// 表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
int n = s.size();
// 初始化false
vector<vector<bool>>dp(n,vector<bool>(n,false));
int ret = 0;
// 注意遍历顺序以及下面的if else 判断以此用到之前的求出的结果
// i从尾部开始控制start,j从i开始控制end到n-1(字符串结尾)
for(int i = n-1; i >= 0;i--){
for(int j = i; j < n ;j++){
// 下面两种情况可以简化简化一下代码
if(s[i] == s[j]){
if(j - i <= 1){// i j指向同一个元素,或为两个相邻元素
ret++;
dp[i][j] = true;
}else if(dp[i+1][j-1]){// i j 距离相差大于1,看里面的区间是否是回文子串
ret++;
dp[i][j] = true;
}
}
}
}
return ret;
}
};
- 双指针
- 从中心点开始扩散,一个值可以作为中心点,两个点也可以作为中心点
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
// 从中心点开始扩散,一个值可以作为中心点,两个点也可以作为中心点
int extend(const string& s,int i ,int j,int n){
int ret = 0;
while(i >= 0 && j < n && s[i] == s[j]){
i--;
j++;
ret++;
}
return ret;
}
int countSubstrings(string s) {
int ret =0 ;
int n = s.size();
for(int i = 0; i < n ; i++){
ret += extend(s,i,i,n);// 一个点做中心点
ret += extend(s,i,i+1,s.size());// 两个点做中心点
}
return ret;
}
};
516. 最长回文子序列
class Solution {
public:
int longestPalindromeSubseq(string s) {
// 字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]
int n = s.size();
vector<vector<int>>dp(n,vector<int>(n,0));
// 初始化,一个字符,最长回文子串就是1
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;// 两个字符相等,+2长度
}else{// 不等,取之前大的
// 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,
// 那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
// 为什么不比较下dp[i+1][j-1],因为已经包含在里面了
dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
}
}
}
// 整条长度 [0,n-1]
return dp[0][n-1];
}
};