题型
动态规划的分类
序列型:问题可以按照某种顺序一步步决策,例如最长上升子序列。
分割型:问题需要将数据分割成多个部分,例如矩阵链乘问题或分割等和子集。
背包型:问题涉及选择与否,例如0/1背包问题、完全背包问题等。
区间型:问题涉及一系列的区间,需要进行区间合并或分割,例如区间合并游戏。
树形DP:问题中的数据结构是树形的,需要在树上进行动态规划,例如树的直径或最大独立集。
状态压缩DP:当状态可以通过位操作压缩表示时,如旅行商问题(TSP)。
解题思路
动态规划解题步骤
定义状态:
确定状态数组表示什么含义。通常状态表示到达当前步骤的最优解。
状态转移方程:
这是动态规划中最核心的部分,需要找出状态之间的关系。例如,dp[i] 可能依赖于 dp[i-1] 或 dp[i-2] 等。
初始化状态:
根据问题的边界条件来初始化状态。例如,最小路径问题中 dp[0][0] 是起始点。
填充状态表:
按照逻辑顺序(通常是自底向上)计算所有状态。
基础动态规划题
509. 斐波那契数
思路:
题目其实已经告诉我们递推公式了。
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1。
我们就考虑怎么去初始化这个dp,然后后续怎么遍历呢?dp解决问题主要就是从下往上解决问题,这样就可以避免重复性。对于这道题是不是就属于从左往右遍历?
完整代码如下
class Solution {
public:
int fib(int n) {
if(n<0){
return 0;
}
vector<int> dp(n + 1);
dp[0] = 0; // 初始化第一个元素
if(n>0){
dp[1] = 1; // 如果大于0 初始化第二个元素
}
for(int i = 2 ; i<=n;i++){
dp[i] = dp[i-1]+dp[i-2]; // 后续dp数组填补
}
for(int i = 0 ;i<=n;i++){
cout<<dp[i]<<" ";
}
return dp[n];
}
};
70. 爬楼梯
该题和斐波那契数列数列一样都是经典dp题库。
完整代码:
class Solution {
public:
int climbStairs(int n) {
if(n<1) return 0; // 确保楼梯大于0
vector<int>dp(n+1); // dp[i] 就代表当 n=i 所保存的方法
dp[1] = 1;
if(n>1) dp[2] = 2; // 大于1 初始化第二个元素
for(int i =3;i<=n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
};
746. 使用最小花费爬楼梯
思路:
想明白一点,对于比如我想到达第三个阶梯,是不是就两种方案:
- 我从第一个阶梯 走两步 花费:dp[i-2]+cost[i-2]
- 我从第二个阶梯 走一步 花费:dp[i-1]+cost[i-1] 所以,我们依旧坚持从下往上的思路,我每次都选择最小花费,那最后不就是最小花费了嘛?
重点:
此处dp代表什么?此处dp代表到达i这个点,我需要花费多少
完整代码:
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int>dp(cost.size()+1);
dp[0] = 0; // `此处dp代表到达i这个点,我需要花费多少`
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.不同路径
思路:
1.先明确dp代表什么,此处代表从左上角到达该点的走路方法数
2.怎么得到递推公式呢? 因为只能向右和向下
就属于 dp[i][j] = dp[i-1][j]+dp[i][j-1];
3.再根据此公式填充 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;
}
// 补充dp表
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];
cout<<dp[i][j]<<" ";
}
}
return dp[m-1][n-1];
}
};
63. 不同路径 II
思路:
和上文的不同路径其实没多大区别。只不过对于障碍物,我们在dp中置为0就好了
递推公式没变化: dp[i][j] = dp[i-1][j]+dp[i][j-1];
我这里还是和上题一样,先初始化行和列,只不过此处我们的 dp[i][0] = dp[i - 1][0],dp[0][j] = dp[0][j - 1]。因为是第一行和第一列嘛。
完整代码:
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));
if (obstacleGrid[0][0] == 1) {
dp[0][0] = 0;
} else {
dp[0][0] = 1;
}
// 初始化
for(int i = 1; i<m;i++){
if (obstacleGrid[i][0] == 1) {
dp[i][0] = 0;
} else {
dp[i][0] = dp[i - 1][0];
}
}
for(int j = 1; j<n;j++){
if (obstacleGrid[0][j] == 1) {
dp[0][j] = 0;
} else {
dp[0][j] = dp[0][j - 1];
}
}
// 填充dp数组
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];
}
}
}
cout<<endl;
for(int i = 0 ;i<obstacleGrid.size();i++){
for(int j = 0; j<obstacleGrid[0].size();j++){
cout<<dp[i][j]<<" ";
}
}
return dp[m-1][n-1];
}
};
343. 整数拆分
思路:
先确定 dp[i] 这个代表 对于 i 这个整数,最大乘积化为多少。
其次,我们怎么从小推出 dp?
dp[i] = j*(i-j), dp[i] = dp[i-j]*j,就这两种方案。
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-1 ; j++){ // `拆分次数,拆分为什么情况`
dp[i] = max(dp[i], max(j*(i-j),dp[i-j]*j));
}
}
return dp[n];
}
};
96.不同的二叉搜索树
什么是二叉搜索树?
二叉搜索树是一种特殊的二叉树,其中每个节点满足以下条件:
左子树中所有节点的值都小于根节点的值。
右子树中所有节点的值都大于根节点的值。
左右子树也分别为二叉搜索树。
思路:
对序列我们任一取它随意一个点 i 作为根节点,则,左子树由 [1, i-1] 构成,右子树由 [i+1, n] 构成。
状态转移方程如下:,这里 dp[i-1] 是左子树的数量,而 dp[n-i] 是右子树的数量。
class Solution {
public:
int numTrees(int n) {
vector<int>dp(n+1);
dp[0] = 1; // `空树也是搜索二叉树`
dp[1] = 1;
for(int nodes = 2; nodes<=n ;nodes++){
for(int root = 1; root <= nodes; root++){
dp[nodes] += dp[root-1]*dp[nodes-root];
}
}
return dp[n];
}
};
01背包问题
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
// `这个是代表后续物品重量大于背包,防止了无效的操作(尝试放入一个根本无法放入的物品)`
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
416. 分割等和子集
回溯法,会产生超时问题
class Solution {
public:
vector<int> path;
bool back(vector<int>& nums,int index,int all){
int res = allpath(path);
if(res == all/2){
return true;
}
for(int i = index;i<nums.size();i++){
path.push_back(nums[i]);
if(back(nums,i+1,all)){
return true;
}
path.pop_back();
}
return false;
}
int allpath( vector<int> path){
int temp = 0;
for(int i = 0; i<path.size();i++){
temp+=path[i];
}
return temp;
}
bool canPartition(vector<int>& nums) {
sort(nums.begin(),nums.end());
int all = 0;
for(int i = 0;i <nums.size();i++){
all+=nums[i];
}
if(all %2 != 0) return false;
return back(nums,0,all);
}
};
此时我们使用dp,但此处我用的是二维dp
class Solution {
public:
bool canPartition(vector<int>& nums) {
sort(nums.begin(),nums.end());
int all = 0; // 总数
for(int num : nums){
all += num;
}
if(all %2 != 0) return false;
int target = all/2; // 总和一半
vector<vector<int>>dp(nums.size()+1,vector<int>(target+1,0)); // dp数组
for(int i= 0;i<=nums.size();i++){
dp[i][0] = 0; // 初始化,背包容量为0的时候
}
// dp 数组填充
for(int i = 1; i<=nums.size();i++){
for(int j = 1; j<=target;j++){
if(j < nums[i-1]){
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = max(dp[i-1][j],dp[i-1][j-nums[i-1]]+nums[i-1]);
}
}
}
if(dp[nums.size()][target] == target){
return true;
}
return false;
}
};
一维dp数组
class Solution {
public:
bool canPartition(vector<int>& nums) {
sort(nums.begin(),nums.end());
int all = 0; // 总数
for(int num : nums){
all += num;
}
if(all %2 != 0) return false;
int target = all/2; // 总和一半
vector<int>dp(target+1,0); // dp数组
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
思路:
这个和上个题分割子问题,没什么区别,就是分为两堆石头,然后差值就是剩下的。
只是再次注意一点,因为是不重复的东西,我们一维dp是从后往前进行遍历的。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int allnum = 0;
for(int num : stones){
allnum += num;
}
int target = allnum/2;
vector<int>dp(target+1,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 allnum - dp[target] - dp[target];
}
};
494.目标和
回溯法:
我的第一个想法就是回溯,逐步进行搜索.
// 回溯法
class Solution {
public:
int count;
void back(vector<int>& nums,int target,int index,int sum){
if(index == nums.size()){
if(sum == target){
count++;
}
return;
}
back(nums,target,index+1,sum-nums[index]);
back(nums,target,index+1,sum+nums[index]);
}
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
back(nums,target,0,sum);
return count;
}
};
动态规划:
此处动态规划是属于那种计数问题,常规递归公式如下
dp[j] += dp[j - nums[i]];
474.一和零
思路:
这里,我们相通一个点,子集字符串就等于物品
背包就是 m 和 n 它的含量.
dp[i][j] = max(dp[i][j],dp[i-zeros][j-ones]+1); 这个就是从后往前遍历,
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& str : strs){
int zeros = count(str.begin(),str.end(),'0');
int ones = str.size() - zeros;
for(int i = m;i>=zeros;i--){
for(int j = n;j>=ones;j--){
dp[i][j] = max(dp[i][j],dp[i-zeros][j-ones]+1);
}
}
}
return dp[m][n];
}
};
完全背包问题
核心代码:
这是属于组合
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
这是属于排列
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
518.零钱兑换II
思路:
我们先分辨出这是个完全背包问题.
dp[j] = dp[j] +dp[j-coins[i]];
// dp[j] 表示之前我没加上 coins[i] 的方法数
// dp[j-coins[i]] 表示我加上 coins[i] 的方法数
// 这样总的叠加就是新的dp[j]
完整代码:
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] 表示之前我没加上 coins[i] 的方法数
// dp[j-coins[i]] 表示我加上 coins[i] 的方法数
// 这样总的叠加就是新的dp[j]
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
};
377. 组合总和 Ⅳ
思路:
这是一个排列问题,.
核心就是,先背包,后物品
完整代码:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<uint64_t>dp(target+1,0);
dp[0] = 1;
for(int j = 0; j<=target ;j++){
for(int i = 0; i<nums.size();i++){
if(j>=nums[i]) dp[j] += dp[j-nums[i]];
}
for(int num : dp){
cout<<num<<' ';
}
cout<<endl;
}
return dp[target];
}
};
322. 零钱兑换
思路:
我的第一个想法就是回溯,代码如下,但是存在问题,就是超出时间限制
class Solution {
public:
int res;
int count = 0;
int minnum = INT_MAX;
void back(vector<int>& coins,int amount,int res,int index,int count){
if(res > amount){
return;
}
if(res == amount){
minnum = min(minnum,count);
return;
}
for(int i = index; i>=0;i--){
res += coins[i];
back(coins,amount,res,i,count+1);
res -= coins[i];
}
}
int coinChange(vector<int>& coins, int amount) {
// 贪心方法
sort(coins.begin(),coins.end());
//if(coins.back() > amount) return -1;
back(coins,amount,0,coins.size()-1,0);
return minnum == INT_MAX ? -1 : minnum;
}
};
279.完全平方数
思路
我们先明白,这是什么问题,完全背包问题
其次,物品是什么?物品就是数字,从1到n开平方;
背包是什么?就是n;
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,n+1);
dp[0] = 0;
for(int i = 1;i*i<=n;i++){
for(int j = i*i;j<=n;j++){
dp[j] = min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
};
139.单词拆分
思路一:回溯法(超出时间限制)
class Solution {
public:
string path;
bool res = false;
void back(vector<string>& wordDict,string s,int index){
if(path == s){
res = true;
return;
}
if(path.size()>s.size()){
return;
}
for(int i = 0;i<wordDict.size();i++){
path.append(wordDict[i]);
back(wordDict,s,i);
path.erase(path.size()-wordDict[i].size(),wordDict[i].size());
}
}
bool wordBreak(string s, vector<string>& wordDict) {
back(wordDict,s,0);
return res;
}
};
思路二:加入记忆搜索的回溯
class Solution {
public:
string path;
bool res = false;
unordered_map<int, bool> memo; // 用于记忆化的哈希表
void back(vector<string>& wordDict, string& s, int index){
if (index == s.size()) {
res = true;
return;
}
if (memo.count(index) && !memo[index]) {
return; // 如果这个子问题已解决且结果为 false,直接返回
}
if (path == s) {
res = true;
memo[index] = true; // 记录成功的分解
return;
}
if (path.size() > s.size()) {
return;
}
for (int i = 0; i < wordDict.size(); i++) {
path.append(wordDict[i]);
if (s.substr(0, path.size()) == path) { // 只有当路径是目标字符串的前缀时才递归
back(wordDict, s, index + wordDict[i].size());
}
path.erase(path.size() - wordDict[i].size(), wordDict[i].size());
if (res) return; // 如果已经找到解,就中断循环
}
memo[index] = res; // 记录当前索引的结果
}
bool wordBreak(string s, vector<string>& wordDict) {
back(wordDict, s, 0);
return res;
}
};
思路三:完全背包.
字符串就是背包
字典就是物品
字符串是由特定物品组成,就等于这还是个排列问题
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size(); // 背包
int m = wordDict.size(); // 物品
vector<bool>dp(n+1,false);
dp[0] = true;
for(int j = 1;j<=n;j++){
for(int i = 1;i<=m;i++){
string word = wordDict[i-1];
if(j >= word.size() && s.substr(j-word.size(),word.size()) == word ){
dp[j] = dp[j] || dp[j-word.size()];
}
}
}
return dp[n];
}
};
多重背包问题
打家劫舍问题
核心代码:
int robRange(vector<int>& nums, int start, int end) {
if (end == start) 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];
}
198.打家劫舍
思路:
首先、我们先搞清楚 dp[i] 代表什么,代表从前 i 个房屋中能够偷取的最大金额。
第一、dp[i - 1] 表示不偷取当前房屋(第 i 个房屋)时,前 i 个房屋能够偷取的最大金额。
第二、dp[i - 2] + nums[i - 1] 表示偷取当前房屋时,前 i 个房屋能够偷取的最大金额。其中 dp[i - 2] 表示不偷取前一个房屋(第 i-1 个房屋),因为连续偷两个相邻的房屋会触发警报,所以要从前前一个房屋(第 i-2 个房屋)开始计算,当前房屋的金额 nums[i - 1]。
完整代码:
class Solution {
public:
int rob(vector<int>& nums) {
vector<int>dp(nums.size()+1,0);
dp[0] = 0;
dp[1] = nums[0];
cout<<dp[1]<<"lala ";
for(int i = 2; i<=nums.size();i++){
dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[nums.size()];
}
};
213.打家劫舍II
思路:
我们把问题分解为两个子问题:
问题一:我们偷取第一个房子,不偷最后一个
问题二:我们不偷第一个,我们偷最后一个
最后比较,谁大返回谁
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n==0) return 0;
if(n == 1) return nums[0];
if(n == 2) return max(nums[0],nums[1]);
// 获取第一个房屋,不偷最后一个
vector<int> dp1(n+1,0);
dp1[0] = 0;
dp1[1] = nums[0];
for(int i = 2 ; i<=n-1 ; i++){
dp1[i] = max(dp1[i-1],dp1[i-2]+nums[i-1]);
}
// 获取最后一个房屋,不偷第一个
vector<int> dp2(n+1,0);
dp2[0] = 0;
dp2[1] = 0;
dp2[2] = nums[1];
dp2[3] = max(nums[1],nums[2]);
for(int i = 4 ; i<=n;i++){
dp2[i] = max(dp2[i-1],dp2[i-2]+nums[i-1]);
}
return max(dp1[n-1],dp2[n]);
}
};
337.打家劫舍 III
思路:
分为两个子问题:
问题一:获取当前根节点最大数
问题二:不获取当前根节点最大数
class Solution {
public:
int rob(TreeNode* root) {
vector<int>res = rogchange(root);
return max(res[0],res[1]);
}
vector<int> rogchange(TreeNode *root){
if(!root) return{0,0};
vector<int>left = rogchange(root->left);
vector<int>right = rogchange(root->right);
//{0,1} 0代表不获取当前节点,1代表获取当前节点数
// 不获取根节点
int dp0 = max(left[0],left[1])+max(right[0],right[1]);
// 获取根节点
int dp1 = root->val + left[0] + right[0];
return {dp0,dp1};
}
};
股票问题
121. 买卖股票的最佳时机
思路一:暴力搜索,会超出时间限制
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 暴力
int res = INT_MIN;
for(int i = 0; i<prices.size();i++){
for(int j = i+1;j<prices.size();j++){
if((prices[j] - prices[i] )>=0){
res = max(res,(prices[j] - prices[i] ));
}
}
}
return res == INT_MIN ? 0:res;
}
};
暴力优化了一下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n<=1) return 0;
int minnum = prices[0];
int res = INT_MIN;
for(int i =1;i<n;i++){
if(prices[i]<minnum){
minnum = prices[i];
}
else{
res = max(res,prices[i]-minnum);
}
}
return res== INT_MIN ? 0:res;
}
};
思路三:动态规划
我们先分解为子问题:
问题一: 当天没股票 我的总金额
是不是没有股票就是, dp[i-1][0](前一天就没有股票), dp[i-1][1]+prices[i])(或者就是前一天室友股票的但是我今天,卖掉)
dp[i][0] = max(dp[i-1][0] , dp[i-1][1]+prices[i]); // 没有股票
问题二: 当天有彩票,我的总金额.
dp[i-1][1](前一天我就已经有了彩票) -prices[i],代表我今天刚买入彩票
dp[i][1] = max(dp[i-1][1],-prices[i]); // 有股票
完整代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
//if (n == 0) return 0;
vector<vector<int>> dp(n+1,vector<int>(2,0));
// 初始化状态 0 代表没有股票,1 代表有股票
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],-prices[i]); // 有股票
}
return dp[n-1][0]; // 返回最后一天没有股票的最大金额
}
};
122.买卖股票的最佳时机II
思路:
我们还是分为两个子问题
问题一:当天是否有股票
问题二:当天是否没股票
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n+1,vector<int>(2,0));
// 0代表没股票,1代表有股票
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
思路一:
我还是分解为子问题
问题一:第一次购买
问题二:第二次购买
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n < 2) return 0;
vector<vector<vector<int>>> dp(n, vector<vector<int>>(3, vector<int>(2, 0)));
// 初始化第一天的情况
dp[0][0][0] = 0;
dp[0][0][1] = -prices[0];
dp[0][1][0] = dp[0][2][0] = 0;
dp[0][1][1] = INT_MIN; // 不可能的状态初始化为一个非常小的数
for (int i = 1; i < n; i++) {
// 第 i 天没有进行任何交易,且不持有股票。
dp[i][0][0] = dp[i-1][0][0];
// 第 i 天没有进行任何交易,但持有股票。
dp[i][0][1] = max(dp[i-1][0][1], dp[i-1][0][0] - prices[i]); // 只买不卖
// 第 i 天进行了一次交易,且不持有股票。
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][0][1] + prices[i]);
// 第 i 天进行了一次交易,持有股票
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][1][0] - prices[i]); // 第一次买入
// 第 i 天进行了两次交易,且不持有股票。
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][1][1] + prices[i]); // 第二次卖出
//dp[i][2][1] = INT_MIN; // 在限制条件下不存在进行第三次买入的可能
}
// 最大利润,可以是0次、1次或2次交易
return max({dp[n-1][0][0], dp[n-1][1][0], dp[n-1][2][0]});
}
};
三维数组优化一下
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n <= 1) return 0;
vector<vector<int>> dp(n, vector<int>(5, 0));
dp[0][0] = 0; // 第一天没有任何交易。
dp[0][1] = -prices[0]; //第一天买入股票
dp[0][2] = 0;// 第一天不可能完成一次卖出,初始化为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 max(dp[n-1][4],dp[n-1][2]);
}
};
188.买卖股票的最佳时机IV
思路:
借鉴上一题,我们同样化为两个子问题
问题一: 单数代表我买股票怎么样操作
问题二:双数代表我卖股票怎么样操作
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
vector<vector<int>>dp(n+1,vector<int>(2*k+1,0));
// 单数代表我买入股票,双数代表我卖出股票
dp[0][0] = 0;
for(int j = 1; j<=2*k ; j+=2){
dp[0][j] = -prices[0];
}
for(int i = 1 ;i<n;i++){
for(int j = 1;j<=2*k;j++){
if(j % 2 == 0){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
}
else{
dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
}
}
}
return dp[n-1][2*k];
}
};
309.最佳买卖股票时机含冷冻期
思路:
直接按照上题的模板写出的
完整代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n<=1) return 0;
// 自动确定最大交易次数
int k = min((n + 1) / 3, n / 2);
vector<vector<int>> dp(n+1,vector<int>(2*k+1,0));
// 初始化
dp[0][0] = 0;
for(int j = 1 ; j<=2*k;j+=2){
dp[0][j] = -prices[0];
}
for(int j = 1 ; j<=2*k;j++){
if(j%2 == 0){
dp[1][j] = max(dp[0][0], dp[0][1] + prices[1]); // 第二天结束时,不持有股票
}
else{
dp[1][j] = max(dp[0][1], -prices[1]); // 第二天结束时,持有股票(直接买入)
}
}
// 数组填充
for(int i = 2;i<n;i++){
for(int j = 1; j<=2*k ;j++){
if(j%2 == 0){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-1] + prices[i]); //卖出
}
else{
dp[i][j] = max(dp[i-1][j],dp[i-2][j-1] - prices[i]); // 买入,隔一天买入
}
}
}
return dp[n-1][2*k];
}
};
优化一下:
思路:
分解为两个子问题
我们先定义两个状态:
dp[i][0]: 第 i 天结束时,不持有股票的最大利润(可能是冷冻期的一天)。
dp[i][1]: 第 i 天结束时,持有股票的最大利润。
问题一:当天没有股票
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]):第 i 天结束时不持有股票的利润来自两种情况,前一天也不持有或者前一天持有但在第 i 天卖出。
问题二:当天有股票
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]):第 i 天结束时持有股票的利润也来自两种情况,前一天持有或者前两天不持有(因为冷冻期)并在第 i 天买入。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n<=1) return 0;
vector<vector<int>>dp(n+1,vector<int>(2,0));
dp[0][0] = 0; // 第一天结束时,不持有股票
dp[0][1] = -prices[0]; // 第一天结束时,持有股票
dp[1][0] = max(dp[0][0],dp[0][1]+prices[1]); // 第二天结束时,不持有股票
dp[1][1] = max(dp[0][1],-prices[1]); // 第二天结束时,持有股票(直接买入)
for(int i = 2 ;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-2][0] - prices[i]); // 持有股票时考虑冷冻期
}
return dp[n-1][0];
714.买卖股票的最佳时机含手续费
思路:
这个和最初的可以任意次数购买股票一样
额外的地方就是再买入的时候加上手续费
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<vector<int>> dp(n+1,vector<int>(2,0));
dp[0][0] = 0;
dp[0][1] = -prices[0] -fee;
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][0];
}
};
股票问题总结
核心代码如下:
k 代表能够买卖几次。
代码初始化如下:
// 单数代表我买入股票,双数代表我卖出股票
dp[0][0] = 0;
for(int j = 1; j<=2*k ; j+=2){
dp[0][j] = -prices[0];
}
dp数组填充如下:
for(int i = 1 ;i<n;i++){
for(int j = 1;j<=2*k;j++){
if(j % 2 == 0){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
}
else{
dp[i][j] = max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
}
}
}
子序列问题
300.最长递增子序列
思路:
我们设定的dp[i]为末尾为 nums[i] 的序列长度
遍历
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if(n<=1) return n;
vector<int>dp(n+1,1);
int res = 1;
// dp[i] 代表 当最长子序列末尾为 nums[i] 的最长子序列
// 填充 dp 数组
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);
}
}
res = max(res,dp[i]);
}
return res;
}
};
674. 最长连续递增序列
思路:
我们是需要连续的,核心代码如下
if(nums[i]>nums[i-1]){
dp[i] = dp[i-1]+1;
}
dp[i] 代表最长子序列末尾为 nums[i] 这个数
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int n = nums.size();
if(n<=1) return n;
vector<int>dp(n+1,1);
int res = 1;
for(int i = 1;i<n;i++){
if(nums[i]>nums[i-1]){
dp[i] = dp[i-1]+1;
}
res = res>dp[i]?res:dp[i];
}
return res;
}
};
718. 最长重复子数组
思路:
dp[i][j] 代表什么?代表 以 nums1[i-1] 和 nums2[j-1] 结尾的最长公共子数组的长度
完整代码:
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size();
int m = nums2.size();
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
int res = 0;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
res = max(res,dp[i][j]);
}else{
dp[i][j] = 0;
}
}
}
return res;
}
};
1143.最长公共子序列
思路:
dp[i][j] = dp[i-1][j-1] + 1;
如果 text1[i-1] 和 text2[j-1] 相同,说明当前两个字符可以作为最长公共子序列的一部分。此时,dp[i][j] 应该是不包括这两个字符的子序列长度 dp[i-1][j-1] 加上这两个匹配的字符(即加1)
dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
dp[i-1][j]:这代表包含 text1 中前 i-1 个元素和 text2 中前 j 个元素的最长公共子序列。这意味着我们正在考虑忽略 text1[i-1] 的情况。
dp[i][j-1]:这代表包含 text1 中前 i 个元素和 text2 中前 j-1 个元素的最长公共子序列。这意味着我们正在考虑忽略 text2[j-1] 的情况。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size();
int m = text2.size();
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
int res = 0;
for(int i = 1;i<=n ;i++){
for(int j= 1;j<=m;j++ ){
if(text1[i-1] == text2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
//res = max(res,dp[i][j]);
}else{
dp[i][j] = max(dp[i][j-1],dp[i-1][j]);
}
res = max(res,dp[i][j]);
}
}
return res;
}
};
1035.不相交的线
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int n = nums1.size();
int m = nums2.size();
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
int index = 1;
int res = 0;
for(int i = 1;i<=n;i++){
for(int j = index;j<=m;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[n][m];
}
};
53. 最大子序和
思路:
dp[i] 表示以 nums[i] 结尾的最大子数组和
如果 dp[i - 1](即以 nums[i-1] 结尾的最大子数组和)小于0,那么任何包含 nums[i-1] 的子数组都不如 nums[i] 本身,所以从 nums[i] 开始一个新的子数组。
否则,将 nums[i] 添加到现有子数组中
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n==0) return 0;
if(n==1) return nums[0];
vector<int>dp(n+1,0);
dp[0] = nums[0];
int res = dp[0]; // 初始化最大和为第一个元素
for(int i = 1; i<n;i++){
if(dp[i-1]<0){
dp[i] = nums[i];
}
else{
dp[i] = dp[i-1]+nums[i];
}
res = max(res,dp[i]);
}
return res;
}
};
392.判断子序列
思路:
最简单的方法,双指针法,一步一步往前移动
class Solution {
public:
bool isSubsequence(string s, string t) {
int n = s.size();
int m = t.size();
int sindex = 0;
int tindex = 0;
while(sindex<n && tindex<m){
if(s[sindex] == t[tindex]){
sindex++;
}
tindex++;
}
return sindex == n;
}
};
思路二:动态规划
这里是引用
class Solution {
public:
bool isSubsequence(string s, string t) {
int n = s.size();
int m = t.size();
if(n==0) return true;
if(m==0 && n != 0) return false;
vector<bool> dp(n+1,false);
dp[0] = true;
int index = 1;
for(int i = 1;i<=n;i++){
for(int j = index;j<=m;j++){
if(s[i-1] == t[j-1]){
dp[i] = dp[i-1] && true;
index = j+1;
break;
}
}
}
return dp[n];
}
};
115.不同的子序列
思路一:回溯法(会超过时间限制)
class Solution {
public:
string path;
int count;
void back(string s,string t,int index){
if(path == t){
count++;
}
for(int i = index;i<s.size();i++){
path.push_back(s[i]);
back(s,t,i+1);
path.pop_back();
}
}
int numDistinct(string s, string t) {
//int count = 0;
back(s,t,0);
return count;
}
};
思路二:动态规划
dp[i][j] 表示考虑 s 的前 i 个字符和 t 的前 j 个字符时,t 作为 s 的子序列出现的次数。
分解为子问题
当 s[i-1] == t[j-1] 时,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]。这意味着当前字符可以作为一个新的子序列的结尾(dp[i-1][j-1]),加上我们可以忽略这个字符(dp[i-1][j])。
当 s[i-1] != t[j-1] 时,dp[i][j] = dp[i-1][j],即忽略 s 的当前字符。
class Solution {
public:
int numDistinct(string s, string t) {
int n = s.size();
int m = t.size();
// 初始化
vector<vector<unsigned long long >>dp(n+1,vector<unsigned long long >(m+1,0));
for(int i = 0;i<=n;i++){
dp[i][0] = 1;
}
// 填充 dp 数组
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;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];
}
}
}
// for(int i = 0;i<=n;i++){
// for(int j= 0;j<=m;j++){
// cout<<dp[i][j]<<" ";
// }
// cout<<endl;
// }
return dp[n][m];
}
};
583. 两个字符串的删除操作
思路:
初始化:
dp[i][0] = i:如果 word2 是空字符串,将 word1 转换成空字符串需要的最少操作次数就是删除所有字符,因此操作次数等于 word1 的长度。
dp[0][j] = j:同理,如果 word1 是空字符串,那么将空字符串转换成 word2 需要的最少操作次数就是插入所有 word2 的字符。
填充动态规划表:
如果当前字符匹配(word1[i-1] == word2[j-1]),则不需要额外操作,直接继承左上角的值(dp[i-1][j-1])。
如果当前字符不匹配,则需要考虑三种操作:
删word1[i - 1],最少操作次数为dp[i - 1][j] + 1。
删word2[j - 1],最少操作次数为dp[i][j - 1] + 1
同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size();
int m = word2.size();
// 初始化
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
for(int i = 0; i<=n;i++){
dp[i][0] = i;
}
for(int j = 0 ;j<=m; j++){
dp[0][j] = j;
}
// 填充数组
for(int i = 1 ;i<=n;i++){
for(int j= 1;j<=m;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,dp[i-1][j-1]+2});
}
}
}
return dp[n][m];
}
};
72. 编辑距离
思路:
dp[i][j]表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最小操作数
如果 word1[i-1] 等于 word2[j-1](字符相同,无需操作):
dp[i][j] = dp[i-1][j-1]
否则(字符不同,需要一次操作):
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
dp[i-1][j] + 1: 从 word1 中删除一个字符
dp[i][j-1] + 1: 向 word1 中插入一个字符
dp[i-1][j-1] + 1: 替换 word1 中的一个字符
删除操作 (dp[i-1][j] + 1):
状态说明:dp[i-1][j] 表示将 word1 的前 i-1 个字符转换成 word2 的前 j 个字符所需的最小操作数。
操作意义:若要从 dp[i-1][j] 转变到 dp[i][j],意味着我们要将 word1 的第 i 个字符删除,使得 word1 的前 i-1 个字符匹配 word2 的前 j 个字符。因此,在 dp[i-1][j] 的基础上增加一次删除操作(+1)。
插入操作 (dp[i][j-1] + 1):
状态说明:dp[i][j-1] 表示将 word1 的前 i 个字符转换成 word2 的前 j-1 个字符所需的最小操作数。
操作意义:要从 dp[i][j-1] 转变到 dp[i][j],意味着我们需要在 word1 的第 i 个字符后插入 word2 的第 j 个字符,使得 word1 的前 i 个字符加上这个新插入的字符能匹配 word2 的前 j 个字符。因此,我们在 dp[i][j-1] 的基础上增加一次插入操作(+1)。
替换操作 (dp[i-1][j-1] + 1):
状态说明:dp[i-1][j-1] 表示将 word1 的前 i-1 个字符转换成 word2 的前 j-1 个字符所需的最小操作数。
操作意义:要从 dp[i-1][j-1] 转变到 dp[i][j],如果 word1[i-1] 不等于 word2[j-1],我们可以通过替换 word1 的第 i 个字符(实际上是 i-1 位置上的字符)为 word2 的第 j 个字符(实际上是 j-1 位置上的字符)来实现这一目标。这样 word1 的前 i 个字符就能匹配 word2 的前 j 个字符了。因此,我们在 dp[i-1][j-1] 的基础上增加一次替换操作(+1)。
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size();
int m = word2.size();
// 初始化
vector<vector<int>> dp(n+1,vector<int>(m+1));
for(int i = 0;i<=n;i++){
dp[i][0] = i;
}
for(int j = 0 ;j<=m;j++){
dp[0][j] = j;
}
// 填充数组
for(int i = 1;i<=n;i++){
for(int j = 1; j<=m;j++){
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = min({dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1});
}
}
}
return dp[n][m];
}
};
回文
647. 回文子串
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int count = 0;
// 单字符回文
for(int i = 0 ;i<n;i++){
dp[i][i] = true;
count++;
}
// 两个连续的字符回文
for(int i =0;i<n-1;i++){
if(s[i] == s[i+1]){
dp[i][i+1] = true;
count++;
}
}
// 数组填充
for(int len = 3;len<=n;len++){
for(int i = 0;i<n-len+1;i++){
int j = i+len-1;
if(s[i] == s[j] && dp[i+1][j-1]==true){
count++;
dp[i][j]=true;
}
}
}
return count;
}
};
516.最长回文子序列
dp[i][j] 为子字符串 s[i] 到 s[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) {
int n = s.size();
// 初始化
vector<vector<int>>dp(n+1,vector<int>(n+1,0));
for(int i = 0;i<n;i++){
dp[i][i] = 1;
}
for(int len = 2;len<=n;len++){
for(int i= 0;i<=n-len;i++){
int j = i+len-1;
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];
}
};