目录
- 动态规划进阶:深度解析序列决策与状态机模型
-
- [一、 "打家劫舍"模型:状态拆分的起点](#一、 “打家劫舍”模型:状态拆分的起点)
- [二、 "买卖股票"模型:多状态机的演进](#二、 “买卖股票”模型:多状态机的演进)
动态规划进阶:深度解析序列决策与状态机模型
在处理具有复杂限制条件的动态规划问题时,单一的状态定义往往难以涵盖所有决策分支。通过将每一天的局面拆解为多个互斥的快照(States) ,并利用状态机描述其间的转移逻辑,可以有效地消除后效性,使问题迎刃而解。
一、 "打家劫舍"模型:状态拆分的起点
这类题目的核心在于处理"相邻元素互斥"的约束。
题目:198.打家劫舍

-
状态表示:
-
f i fi fi:表示偷到第 i i i 个位置时,确定偷 n u m s i numsi numsi,此时的最大金额。
-
g i gi gi:表示偷到第 i i i 个位置时,确定不偷 n u m s i numsi numsi,此时的最大金额。
-
状态转移方程:基于"最后一步"的决策逻辑:
-
f i = g i − 1 + n u m s i fi = gi-1 + numsi fi=gi−1+numsi:若偷当前房,前一间房必须处于"不偷"状态。
-
g i = max ( f i − 1 , g i − 1 ) gi = \max(fi-1, gi-1) gi=max(fi−1,gi−1):若不偷当前房,前一间房偷或不偷均可,取最大值即可。
-
初始化 : f 0 = n u m s 0 f0 = nums0 f0=nums0, g 0 = 0 g0 = 0 g0=0。

代码实现:
cpp
class Solution
{
public:
int rob(vector<int>& nums)
{
//跟按摩师一样
int n=nums.size();
vector<int> f(n);
auto g=f;
f[0]=nums[0];
for(int i=1;i<n;++i)
{
f[i]=g[i-1]+nums[i];
g[i]=max(f[i-1],g[i-1]);
}
return max(f[n-1],g[n-1]);
}
};
题目:213.打家劫舍II

- 核心痛点:第一间房与最后一间房首尾相连,产生约束冲突。
- 化圆为线策略:通过"分类讨论"消除环形约束:
- 情况 A :不考虑第一间房,在区间 1 , n − 1 1, n-1 1,n−1 执行上述逻辑。
- 情况 B :不考虑最后一间房,在区间 0 , n − 2 0, n-2 0,n−2 执行上述逻辑。
- 结论 :最终结果为 max ( 情况 B , 情况 A ) \max(\text{情况 B}, \text{情况 A}) max(情况 B,情况 A)。

代码实现:
cpp
class Solution
{
public:
int rob(vector<int>& nums)
{
//处理首位房子后确定进行打家劫舍的区间
//转化为打家劫舍1(找到可以进行打家劫舍1的区间)
int n=nums.size();
return max(nums[0]+rob1(nums,2,n-2),rob1(nums,1,n-1));
}
int rob1(vector<int>& nums,int left,int right)
{
if(left>right)return 0;
int n=nums.size();
vector<int> f(n);
auto g=f;
f[left]=nums[left];
for(int i=left+1;i<=right;++i)
{
f[i]=g[i-1]+nums[i];
g[i]=max(f[i-1],g[i-1]);
}
return max(f[right],g[right]);
}
};
题目:740.删除并获得点数

- 转化思维:这道题看似新颖,实则是"打家劫舍"的变体。
- 预处理 :先统计每个数字出现的总分(点数 = 数字 × \times × 出现次数)。
- 建立映射 :数字 i i i 的选取会导致 i − 1 i-1 i−1 和 i + 1 i+1 i+1 无法选取,这完全等同于"相邻房屋不能同时偷"的逻辑
- 解法 :对统计后的点数数组执行 f i fi fi 与 g i gi gi 的状态转移。

状态表示及状态转移方程

代码实现:
cpp
class Solution
{
public:
int deleteAndEarn(vector<int>& nums)
{
sort(nums.begin(),nums.end());
int n=nums[nums.size()-1]+1;//加上0这个位置
//将原数组的值累加到v的对应位置,之后就可以转变为打家劫舍
vector<int> v(n);
for(int i=0,j=nums[i];i<nums.size();)
{
if(nums[i]==j)v[j]+=nums[i++];
else ++j;
}
//打家劫舍
vector<int> f(n);
auto g=f;
f[1]=v[1],g[1]=0;
for(int i=2;i<n;++i)
{
f[i]=g[i-1]+v[i];
g[i]=max(f[i-1],g[i-1]);
}
return max(f[n-1],g[n-1]);
}
};
二、 "买卖股票"模型:多状态机的演进
股票系列题目引入了冷冻期和交易次数限制,状态机随之变得更加复杂。
题目:309.买卖股票的最佳时期含冷冻期

引入卖出后强制锁定 1 天的规则后,状态表示和状态转移方程如下:
-
状态表示 :第 i i i天结束后,资产处于以下三者之一:
-
d p i 0 dpi0 dpi0(买入状态):手中有一支股票。
-
d p i 1 dpi1 dpi1(可交易状态):手里没股票,且今天没卖出,随时可以买。
-
d p i 2 dpi2 dpi2(冷冻期状态):今天刚卖出股票,明天不能买。

-
状态转移核心:
-
d p i 0 = max ( d p i − 1 0 , d p i − 1 1 − p r i c e s i ) dpi0 = \max(dpi-10, dpi-11 - pricesi) dpi0=max(dpi−10,dpi−11−pricesi)
-
d p i 1 = max ( d p i − 1 1 , d p i − 1 2 ) dpi1 = \max(dpi-11, dpi-12) dpi1=max(dpi−11,dpi−12)
-
d p i 2 = d p i − 1 0 + p r i c e s i dpi2 = dpi-10 + pricesi dpi2=dpi−10+pricesi
根据状态机得出状态转移方程

- 初始化:根据转移方程,只需要初始化0下标位置的值即可;

代码实现:
cpp
class Solution
{
public:
int maxProfit(vector<int>& prices)
{
int n=prices.size();
vector<vector<int>> dp(n,vector<int>(3,0));//0买入1可交易2冷冻期
dp[0][0]=-prices[0];//初始化第一个位置即可(1,2都为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][2]);
dp[i][2]=dp[i-1][0]+prices[i];
}
return max(dp[n-1][1],dp[n-1][2]);//最大值不会在最后一天再买入
}
};
题目:123.买卖股票的最佳时机III

引入了"交易次数 "这一关键属性标签。
- 状态表示:
- : f i j fij fij:第i天结束之后,完成了j次交易,此时处于"买入"状态下的最大利润
- : g i j gij gij:第i天结束之后,完成了j次交易,此时处于"卖出"状态下的最大利润

-
状态转移的奥秘:
-
卖出触发计数 :卖出触发计数: g i j = max ( g i − 1 j , f i − 1 j − 1 + p r i c e s i ) gij = \max(gi-1j, fi-1j-1 + pricesi) gij=max(gi−1j,fi−1j−1+pricesi)。
-
逻辑解析 :当你卖出第 j j j 支股票时,你必须从"已完成 j − 1 j-1 j−1 次交易且持股"的状态转移过来。卖出一瞬间,完成次数从 j − 1 j-1 j−1 跃升为 j j j。
-
初始化 :为了避开非法路径(如未买入先卖出),除 g 0 0 = 0 g00=0 g00=0 和 f 0 0 = − p r i c e s 0 f00=-prices0 f00=−prices0 外,其余均设为极小值( I N F = 0 x 3 f 3 f 3 f INF=0x3f3f3f INF=0x3f3f3f------整形最大值的一半)这样可以避免数据溢出。

- 如何理解 j − 1 j-1 j−1
如果感到困惑是因为你在找" j = j + 1 j = j + 1 j=j+1"这样的加法语句,实际在DP表中, g i j gij gij 依赖 f i − 1 j − 1 fi-1j-1 fi−1j−1 就等同于执行了 j = j + 1 j = j + 1 j=j+1。
- 输入: 已经完成 j − 1 j-1 j−1 次的状态。
- 动作: 卖出(触发一次交易完成)。
- 输出: 填入完成 j j j 次的状态格子里。
代码实现:
cpp
class Solution
{
const int INF=0x3f3f3f;//整形最大值的一半
public:
int maxProfit(vector<int>& prices)
{
int n=prices.size();
vector<vector<int>> f(n,vector<int>(3,-INF));//避免数据溢出
auto g=f;
f[0][0]=-prices[0],g[0][0]=0;//初始化
for(int i=1;i<n;++i)
{
for(int j=0;j<3;++j)
{
f[i][j]=max(f[i-1][j],g[i-1][j]-prices[i]);
//可以通过状态方程来处理边界问题
g[i][j]=g[i-1][j];//处理第一列
if(j>=1)//
{
g[i][j]=max(g[i-1][j],f[i-1][j-1]+prices[i]);
}
}
}
//答案在交易次数为2的那一行
int ret=0;
for(int i=0;i<3;++i)
{
ret=max(ret,g[n-1][i]);
}
return ret;
}
};