动态规划进阶:简单多状态模型

目录

动态规划进阶:深度解析序列决策与状态机模型

在处理具有复杂限制条件的动态规划问题时,单一的状态定义往往难以涵盖所有决策分支。通过将每一天的局面拆解为多个互斥的快照(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;
        
    }
};
相关推荐
菜鸡爱玩21 小时前
线性代数矩阵相乘
线性代数·算法·矩阵
devilnumber1 天前
Java 递归算法 详解 + 核心要点 + 实战运用 + 避坑指南
java·开发语言·算法
unicrom_深圳市由你创科技1 天前
哪些控制逻辑应该放在 PLC,哪些放在上位机?
c++
‎ദ്ദിᵔ.˛.ᵔ₎1 天前
双指针、滑动窗口、前缀和、二分查找 算法
算法
顾北顾1 天前
多头注意力机制
人工智能·深度学习·算法
H178535090961 天前
SolidWorks_基于草图的实体特征20_特征错误排查
算法·3d建模·solidworks
hujinyuan201601 天前
2025年12月中国电子学会青少年机器人技术等级考试试卷(二级) 真题+答案
人工智能·算法·机器人
玖玥拾1 天前
C/C++ 基础笔记(十三)继承
c语言·c++·继承
bIo7lyA8v1 天前
算法复杂度评估的实验统计方法与可视化的技术8
算法
李老师讲编程1 天前
中国电子学会图形化2020.12月Scratch三级考级题
算法·scratch·信息学奥赛·图形化编程·scratch素材