目录
按摩师



class Solution {
public:
int massage(vector<int>& nums) {
int m=nums.size();
if(m==0)return 0;
vector<int> f(m);//选
vector<int> g(m);//不选
f[0]=nums[0];
for(int i=1;i<m;i++){
f[i]=g[i-1]+nums[i];
g[i]=max(f[i-1],g[i-1]);
}
return max(f[m-1],g[m-1]);
}
};
打家劫舍2

本题和按摩师或者打家劫舍1非常相似,这里就是一个环形,我们仅仅只需要分析第0个位置偷还是不偷,如果第0个位置偷,那第1个和最后一个不能偷,如果第0个不偷,那剩下的随意
这样的好处是转换问题
class Solution {
public:
int rob(vector<int>& nums) {
int ret1,ret2;//记录结果
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> f(n); // 选
vector<int> g(n); // 不选
//不偷(1,n-1)
f[1] = nums[1];
for (int i = 2; i < n; i++) {
f[i] = g[i - 1] + nums[i];
g[i] = max(f[i - 1], g[i - 1]);
}
ret1= max(f[n - 1], g[n - 1]);
//偷(2,n-2)
vector<int> x(n); // 选
vector<int> y(n); // 不选
x[2]=nums[2];
for (int i = 2; i < n; i++) {
x[i] = y[i - 1] + nums[i];
y[i] = max(x[i - 1], y[i - 1]);
}
ret2= max(x[n - 2], y[n - 2])+nums[0];
return max(ret1,ret2);
}
};
删除并获得点数
本题的题意就是选择数字,选择了一个数字之后,不能选数字+1和数字-1的数字了(言外之意就是不能选择相邻的数字)
读完题之后有点类似打家劫舍,只不过打家劫舍是连续变大的数字,但这里可能重复且可能不连续且可能不是连续变大
所以需要转化,在增开一个数组,然后映射进去,arr[i]表示当前位置有多少个i
转换成打家劫舍问题即可



class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
int m=nums.size();
int arr[10001]={0};
for(auto e:nums){
arr[e]+=e;
}
//打家劫舍
vector<int> f(10001),g(10001);
f[0]=arr[0];//必选
g[0]=0;//不选
for(int i=1;i<10001;i++){
f[i]=g[i-1]+arr[i];
g[i]=max(f[i-1],g[i-1]);
}
return max(f[10000],g[10000]);
}
};
粉刷房子
本题的题意:给你一个数组,从上往下一行一行的选,每行之间选的数字不能相邻,使得选择的数的总和最小
本质就是带点限制的路径问题
动态表示:dp[i][j]:表示从上往下选到i行j列数字时,最小花费,此时要注意不能选[i-1][j]的
然后最后返回dp最后一行三个位置的最小值即可
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
int n=costs.size();
vector<int> x(n),y(n),z(n);
x[0]=costs[0][0];
y[0]=costs[0][1];
z[0]=costs[0][2];
for(int i=1;i<n;i++){
x[i]=min(y[i-1],z[i-1])+costs[i][0];
y[i]=min(x[i-1],z[i-1])+costs[i][1];
z[i]=min(y[i-1],x[i-1])+costs[i][2];
}
return min(min(x[n-1],y[n-1]),z[n-1]);
}
};
买卖股票的最佳时机含冷冻期
309. 买卖股票的最佳时机含冷冻期 - 力扣(LeetCode)
很明显需要多状态表示,一个状态是无法表示清楚,因为你当天结束之后处于什么状态是题意给出的
所以按照题意,需要三个状态,买入,可交易,冷冻期
最重要的是这三个状态之间如何转换,当出现多状态的时候我们很容易混乱,此时需要状态机

箭头的起点表示昨天处于什么状态,经过今天的操作之后,变为什么状态
所以我们要考虑每个状态可以由其他或者自己的状态转换吗
比如昨天处于买入状态,经过今天的卖出,得到利润之后,今天结束处于冷冻期状态
就这样推出所有的状态,然后写出状态转移方程即可

此时需要开3xn的空间,0表示买入状态,1表示可交易状态,2表示冷冻期
那dp[0][j]:表示当天结束之后,处于买入状态,此时的最大利润
那dp[1][j]:表示当天结束之后,处于可交易状态
那dp[2][j]:表示当天结束之后,处于冷冻期状态
那么状态转移方程很容易得出
dp[0][j]=max(dp[1][j-1]-prices[j],dp[0][j-1]);
dp[1][j]=max(dp[1][j-1],dp[2][j-1]);
dp[2][j]=dp[0][j-1]+prices[j];
class Solution {
public:
int maxProfit(vector<int>& prices) {
int m=prices.size();
vector<vector<int>> dp(3,vector<int>(m));
dp[0][0]=0-prices[0];
dp[1][0]=0;
dp[2][0]=0;
for(int j=1;j<m;j++){
dp[0][j]=max(dp[1][j-1]-prices[j],dp[0][j-1]);
dp[1][j]=max(dp[1][j-1],dp[2][j-1]);
dp[2][j]=dp[0][j-1]+prices[j];
}
return max(max(dp[0][m-1],dp[1][m-1]),dp[2][m-1]);
}
};
买卖股票的最佳时机含手续费
714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)
这个对比上一题较为简单,只需要两个状态即可,因为没有冷冻期
所以直接推导状态转移方程即可
分买入和可交易状态
买入只要不卖出仍然处于买入状态+可交易状态经过买入之后到达买入状态
可交易状态只要不交易仍然处于可交易状态+买入经过卖出后到达可交易状态
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int m=prices.size();
vector<vector<int>> dp(2,vector<int>(m));
dp[0][0]=0-fee-prices[0];
for(int j=1;j<m;j++){
//分买入和可交易状态
dp[0][j]=max(dp[0][j-1],dp[1][j-1]-fee-prices[j]);
dp[1][j]=max(dp[0][j-1]+prices[j],dp[1][j-1]);
}
return max(dp[0][m-1],dp[1][m-1]);
}
};
买卖股票的最佳时机Ⅲ
123. 买卖股票的最佳时机 III - 力扣(LeetCode)
注意题意说最多交易两次,这是说明可以交易0次交易1次交易2次,并且不能连续买入,必须等到上一次买入卖出后才能再次买入
根据经验+题目要求,这种一维的题意,dp表不一定是一维,要根据题目来
假设dp[i]表示:第i天结束之后,所能获得的最大利润
很明显状态少了,因为第i天结束之后,你可能处于买入状态,也可能处于卖出状态
所以至少是f[i]和g[i],表示两个结束之后的状态
但是这里题意要求最多两次交易,所以还需要增加状态
f[i][j]:表示第i天结束之后,完成了j次交易,此时处于买入状态,所能获得的最大利润
g[i][j]:表示第i天结束之后,完成了j次交易,此时处于卖出状态,所能获得的最大利润

当状态过多的时候,需要根据状态机不重不漏的推出状态转移方程
状态机中的边表示,起点表示昨天,边表示今天的操作,箭头表示结束后的状态
(昨天经过了今天的什么操作之后变为了什么状态)
比如f[i][j]:很明显只要i-1天处于买入,i天什么都不做也处于买入,此时交易次数不变
昨天是卖出,今天买入之后就会处于买入状态,此时交易次数也不变
只有当昨天处于买入,今天卖出之后交易次数才增加

初始化中两个细节,为了不能越界操作
1:可以看到f中可能会访问i-1,g当中会访问i-1和j-1,此时填表时先填第一行
j-1的意思是当状态g[i][0]的时候才会越界,并且此时的意思是昨天的交易次数是-1,经过今天的卖出之后交易次数+1变为0处于卖出状态,很明显交易次数不可能为-1
所以防止越界操作,我们可以增加if判断,当j-1>=0时才对比max
2:初始化第一行时,不要设为-INT_MIN,因为可能会导致溢出,g[i-2][j]-p[i]这里做减法可能会溢出,所以我们可以设一个很大的负数,大到题目怎么减也不会溢出
也就是-0x3f3f3f3f,这个是-INT_MIN的一半


返回值是g表,因为如果是f表,你手里有股票,怎么样都不是最大例利润
class Solution {
public:
int MIN=-0x3f3f3f3f;
int maxProfit(vector<int>& prices) {
int m=prices.size();
vector<vector<int>> f(m,vector<int>(3));
vector<vector<int>> g(m,vector<int>(3));
//初始化
f[0][0]=-prices[0],f[0][1]=MIN,f[0][2]=MIN;
g[0][0]=0,g[0][1]=MIN,g[0][2]=MIN;
for(int i=1;i<m;i++){
for(int j=0;j<3;j++){
f[i][j]=max(f[i-1][j],g[i-1][j]-prices[i]);
if(j-1>=0){
g[i][j]=max(g[i-1][j],f[i-1][j-1]+prices[i]);
}
else{
g[i][j]=g[i-1][j];
}
}
}
return max(max(g[m-1][0],g[m-1][1]),g[m-1][2]);
}
};
其中的if判断就是刚刚说的优化点,之前说的是新增一行或者一列,但是需要注意下标的映射关系,这里是直接if判断了
还有就是MIN的取值是-0x3f3f3f3f
初始化那里注意一下怎么取值不影响下面的填表
买卖股票的最佳时机Ⅳ
188. 买卖股票的最佳时机 IV - 力扣(LeetCode)
这道题和Ⅲ一样的,Ⅲ讲解的时候已经抽象为j次交易次数了,所以仅仅把常数2的地方更改为k次即可
k可以取小一点,因为天数限制了最大交易次数
class Solution {
public:
const int MIN=-0x3f3f3f3f;
int maxProfit(int k, vector<int>& prices) {
int m = prices.size();
k=min(k,m/2);
vector<vector<int>> f(m, vector<int>(k+1,MIN));
vector<vector<int>> g(m, vector<int>(k+1,MIN));
// 初始化
f[0][0] = -prices[0];
g[0][0]=0;
for (int i = 1; i < m; i++) {
for (int j = 0; j <= k; j++) {
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);
if (j - 1 >= 0) {
g[i][j] = max(g[i - 1][j], f[i - 1][j - 1] + prices[i]);
} else {
g[i][j] = g[i - 1][j];
}
}
}
int ret=0;
for(int i=0;i<=k;i++){
ret=max(ret,g[m-1][i]);
}
return ret;
}
};
以下是记忆化搜索的方案
class Solution {
public:
const int INF = 0x3f3f3f3f;
vector<vector<vector<int>>> memo; // 备忘录:memo[i][j][hold]
int maxProfit(int k, vector<int>& prices) {
int m = prices.size();
// 边界:空数组/1天/0次交易,直接返回0
if (m <= 1 || k == 0) return 0;
// 优化:k≥m/2时等价于无次数限制,用贪心(递归也能处理,但贪心更快)
if (k >= m / 2) {
int res = 0;
for (int i = 1; i < m; i++) {
if (prices[i] > prices[i-1]) res += prices[i] - prices[i-1];
}
return res;
}
// 初始化备忘录:三维数组,初始值为-INF(标记未计算)
// 维度:天数m × 剩余交易次数k+1 × 持有状态2(0/1)
memo = vector<vector<vector<int>>>(m, vector<vector<int>>(k+1, vector<int>(2, -INF)));
// 递归入口:第0天,剩余k次交易,不持有股票
return dfs(0, k, 0, prices);
}
// 递归函数:从第i天开始,剩余j次交易,hold=0/1的最大收益
int dfs(int i, int j, int hold, vector<int>& prices) {
// 边界1:遍历完所有天数
if (i == prices.size()) return 0;
// 边界2:无剩余交易次数,且不持有 → 收益0;持有 → 只能烂手里,收益0(不如不买)
if (j == 0) return hold ? 0 : 0;
// 记忆化:已计算过的子问题,直接返回
if (memo[i][j][hold] != -INF) return memo[i][j][hold];
int res = 0;
if (hold == 0) {
// 不持有:可选「不操作」或「买入」
res = max(
dfs(i+1, j, 0, prices), // 不操作
dfs(i+1, j, 1, prices) - prices[i] // 买入(花钱)
);
} else {
// 持有:可选「不操作」或「卖出」(卖出扣1次交易)
res = max(
dfs(i+1, j, 1, prices), // 不操作
dfs(i+1, j-1, 0, prices) + prices[i] // 卖出(赚钱,剩余交易-1)
);
}
// 记录结果到备忘录
memo[i][j][hold] = res;
return res;
}
};
hold=0和1是区分了f表和g表
总结
这里主要学习了简单多状态dp问题
这类问题是 DP 入门到进阶的核心,特征是「状态维度少(1-3 维)、状态类型有限(2-3 种)、转移逻辑清晰」
维度 单状态 DP(如不同路径) 简单多状态 DP(如股票 / 按摩师) 状态定义 仅 1 种状态(如 dp[i]= 到 i 的路径数)多种互斥状态(如 持有/不持有「选 / 不选」)状态转移 仅依赖前序 1 种状态的转移 不同状态间相互转移(如持有→不持有,不持有→持有) 核心难点 找状态转移方程 定义清晰的状态类型 + 梳理状态间的转移逻辑
- 二元状态:选 / 不选(按摩师、打家劫舍)、持有 / 不持有(股票系列)、冷冻 / 非冷冻(股票含冷冻期);
- 三元状态:持有 / 非冷冻不持有 / 冷冻不持有(股票冷冻期);
- 分层状态:交易次数 + 持有 / 不持有(最多 k 次交易)。
这类问题的解法高度模板化,按以下 5 步走,能解决 90% 的简单多状态 DP 问题:
步骤 1:定义 “互斥且全覆盖” 的状态(核心中的核心)
核心原则:
互斥:不同状态之间无重叠(比如 “持有股票” 和 “不持有股票” 不可能同时发生);
全覆盖:所有可能的情况都能被状态包含(比如股票问题中,任意一天只有 “持有 / 不持有” 两种情况);
少而精:状态数越少越好(优先 2-3 种,避免冗余)。
举例:
按摩师:
f[i](选第 i 个)、g[i](不选第 i 个);股票冷冻期:
dp[0][i](持有)、dp[1][i](非冷冻不持有)、dp[2][i](冷冻不持有);最多 k 次交易:
dfs(i,j,hold)(第 i 天、剩余 j 次、是否持有)。步骤 2:推导状态转移方程(枚举所有合法操作)
核心逻辑:对每个状态,枚举 “当前能做的所有合法操作”,转移到下一个状态,并取最优值(最大 / 最小);
推导模板:
plaintext
对于状态S(i): S(i) = max/min( 操作1 → 转移到S(i)的前驱状态值 + 操作收益, 操作2 → 转移到S(i)的另一前驱状态值 + 操作收益, ... )举例(股票含冷冻期):
持有(dp [0][i]):要么前一天持有(不操作),要么前一天非冷冻不持有(买入)→
max(dp[0][i-1], dp[1][i-1]-prices[i]);冷冻不持有(dp [2][i]):只能前一天持有(卖出)→
dp[0][i-1]+prices[i]。步骤 3:初始化边界状态(避免越界 / 逻辑错误)
核心原则:
初始状态必须是 “绝对合法” 的(比如第 0 天持有股票,收益只能是
-prices[0]);非法状态用
0x3f3f3f3f/-0x3f3f3f3f标记(比如第 0 天不可能冷冻,设为负无穷);空数组 / 边界输入(如 k=0、m=0)需单独处理。
避坑点:
不要漏初始化 “0 次交易”“第 0 天” 等基础状态;
不要用未初始化的状态参与转移
步骤 4:确定结果的取值范围(遍历所有合法最终状态)
核心原则:最终结果是 “所有合法最终状态的最优值”,而非单一状态;
避坑点:
股票问题中,最后一天持有股票无意义,只需比较 “不持有” 的所有状态;
最多 k 次交易中,需遍历 0~k 次交易的状态,而非仅取 k 次(比如 k=2 但 1 次交易收益更高)。
步骤 5:优化(可选,优先实现再优化)
空间优化:若状态仅依赖前一步(如
dp[i]仅依赖dp[i-1]),用变量替代数组(如按摩师问题从 O (n)→O (1));时间优化:特殊场景下的贪心替代(如 k≥m/2 时的股票问题);
记忆化优化:递归解法中必须加备忘录,避免重复子问题。
三、高频避坑要点
状态定义模糊:比如把 “交易次数” 的扣减时机搞混(买入扣 / 卖出扣),导致转移逻辑全错;→ 解决:先写清 “交易次数按什么算”(比如 “一次完整交易 = 买入 + 卖出,卖出时扣”),再定义状态。
转移时访问未初始化状态:比如用
dp[0][j](未初始化)参与max比较;→ 解决:先初始化基础状态(如 0 次交易),再按顺序转移。结果取值错误:比如仅取 k 次交易的状态,忽略 0~k-1 次;→ 解决:遍历所有合法的最终状态,取最优值。
边界未处理:比如空数组、k=0、m=1 时直接访问数组;→ 解决:开头先判断边界条件,直接返回 0。
递归栈溢出:纯递归解 m=1e3 的问题;→ 解决:要么转递推 DP,要么对 k≥m/2 的场景做贪心优化。
四、简单多状态 DP 的核心思维:“状态机”
所有简单多状态 DP 问题,本质都可以抽象成有限状态机(FSM):
每个状态是状态机的一个节点;
状态转移是节点间的有向边(边的权重是操作的收益);
解题就是 “遍历状态机,找到从初始节点到最终节点的最优路径”。
简单多状态 DP 问题的关键就是:同一时间点(比如 “第 i 天”)只会处于「唯一的一个状态」,所有状态彼此互斥,且你必须把这些互斥状态全部罗列出来(不重不漏) —— 这是这类问题能被 DP 解决的核心前提。

1800



被折叠的 条评论
为什么被折叠?



