动态规划之状态机模型:股票买卖II

题目描述

给定一个长度为 NNN 的数组,数组中的第 iii 个数字表示一个给定股票在第 iii 天的价格。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

输入格式

第一行包含整数 NNN,表示数组长度。

第二行包含 NNN 个不超过 10000 的正整数,表示完整的数组。

输出格式

输出一个整数,表示最大利润。

数据范围

1≤N≤1051≤N≤10^51N105

输入样例

5
1 2 3 0 2

输出样例

3

样例解释

对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出],第一笔交易可得利润 2-1 = 1,第二笔交易可得利润 2-0 = 2,共得利润 1+2 = 3。

算法思想

根据题目描述,在NNN天里不限制交易次数,但是必须满足

  • 不能同时参与多笔交易
  • 卖出股票后,无法在第二天买入股票 。

如果将每一天作为一个阶段进行处理,对每一个阶段来说,可以进行股票交易、买入卖出,也可能是冷冻期、无法买入卖出。因此可以借助状态机的思想,将每天的状态细分为冷冻期外不持有股票(空仓)、冷冻期内不持有股票(冷冻期),手中持有股票(持仓) 3种状态,如下图所示:
在这里插入图片描述
上述状态机描述了5种符合条件的状态转换:

  • 没有股票(空仓)-> 没有股票(空仓),继续空仓
  • 没有股票(冷冻期)-> 没有股票(空仓),等待
  • 持有股票(持仓)-> 持有股票(持仓),继续持有
  • 没有股票(空仓)-> 持有股票(持仓),买入
  • 持有股票(持仓)-> 没有股票(冷冻期),清仓卖出

因此,可以使用该状态机模型定义每个阶段的状态,并进行状态计算。

状态表示

  • f[i][0]表示在第i天空仓(不在冷冻期、不持有股票)时,前i天的最大收益
  • f[i][1]表示第i天为冷冻期(不持有股票)时,前i天的最大收益
  • f[i][2]表示在第i天持仓(持有股票)时,前i天的最大收益

状态计算

  • f[i][0]取第i - 1天空仓、或者第i - 1天为冷冻期时,前i - 1天的最大收益:f[i][0] = max(f[i - 1][0], f[i - 1][1])
  • f[i][1]取第i - 1天卖出股票时,前i - 1天的最大收益 + 第i天卖出股票的收益:f[i][1] = f[i - 1][2] + w[i]
  • f[i][2]取第i - 1天持仓、或者在第i - 1天买入股票时的前i - 1天的最大收益减去卖出股票时的收益:f[i][2] = max(f[i - 1][2], f[i - 1][0] - w[i])

初始状态

f[0][0] = 0表示最初不进行任何交易时空仓的最大收益为0
f[0][1] = f[0][2] = -INF表示f[0][1]f[0][2]都是不合法状态。

时间复杂度

状态数:n×3n \times 3n×3
转移计算:O(1)O(1)O(1)
时间复杂度:O(n×3)O(n \times 3)O(n×3)

代码实现

#include <iostream>
#include <cstdio>
using namespace std;
const int N = 100010, INF = 0x3f3f3f3f;
//f[i][0]表示在第i天空仓(不在冷冻期、不持有股票)时,前i天的最大收益
//f[i][1]表示第i天为冷冻期(不持有股票)时,前i天的最大收益
//f[i][2]表示在第i天持仓(持有股票)时,前i天的最大收益
int w[N], f[N][3];
int main()
{
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++) scanf("%d", &w[i]);    
    //初始状态
    //f[0][0]表示最初不进行任何交易时空仓的最大收益为0
    //f[0][1]、f[0][2]都是不合法状态
    f[0][0] = 0, f[0][1] = f[0][2] = -INF;
    for(int i = 1; i <= n; i ++)
    {
        //f[i][0]取第i - 1天空仓、或者第i - 1天为冷冻期时,前i - 1天的最大收益
        f[i][0] = max(f[i - 1][0], f[i - 1][1]);
        //f[i][1]取第i - 1天卖出股票时,前i - 1天的最大收益 + 第i天卖出股票的收益
        f[i][1] = f[i - 1][2] + w[i];
        //f[i][2]取第i - 1天持仓,或者第i - 1天买入股票时的前i - 1天的最大收益
        f[i][2] = max(f[i - 1][2], f[i - 1][0] - w[i]);
    }    
    printf("%d\n", max(f[n][0], f[n][1]));
    
    return 0;
}
<think>好的,用户问动态规划状态转移方程是怎么得出的,我需要详细解释这个过程。首先得明确动态规划的基本概念,然后分步骤说明如何构建状态转移方程。可能用户是刚开始学算法,需要从基础讲起。 首先,应该解释什么是动态规划,以及状态转移方程的重要性。然后分步骤讲步骤:确定状态定义、分析状态间的关系、处理边界条件、优化等。每个步骤要举例说明,比如用斐波那契或者背包问题作为例子,这样更直观。 需要注意用户可能对如何从问题抽象出状态不太清楚,所以得强调如何分解问题,找到子问题。可能还要提到不同的情况,比如线性DP、区间DP等,但例子要简单,避免复杂化。 另外,用户可能想知道常见的错误或难点,比如状态定义不当导致方程复杂,或者重叠子问题处理。需要指出这些点,并给出建议,比如多练习经典问题。 最后总结一下步骤,帮助用户形成系统化的思路。保持语言口语化,不用专业术语堆砌,让解释更易懂。</think>动态规划状态转移方程的推导是算法设计中的核心步骤,需要结合问题特征进行系统性拆解。以下是分步拆解思路和实际案例说明: --- ### **一、明确问题结构(关键起点)** **典型思考**:"这个问题是否满足最优子结构?重叠子问题是否存在?" - ✅ 最优子结构:当前最优解能由子问题最优解推导 - ✅ 重叠子问题:递归求解时存在重复计算 - 🛑 若不具备这两个特性,则不适合用DP **案例**:爬楼梯问题(每次1/2步,求到n阶的方法数) - 到第n阶的方式 = 第n-1阶跨1步 + 第n-2阶跨2步(存在子问题重叠) --- ### **二、状态定义的三种常用维度** 1. **单序列维度** `dp[i]`表示前i个元素的最优解 **案例**:最长递增子序列 → `dp[i]`表示以第i个元素结尾的LIS长度 2. **双序列维度** `dp[i][j]`表示处理到序列1第i位、序列2第j位时的状态 **案例**:编辑距离 → `dp[i][j]`表示word1前i字符转成word2前j字符的最小操作 3. **状态机维度** 增加维度记录附加状态(如持有股票、剩余操作次数等) **案例**:买卖股票最佳时机 → `dp[i][k][0/1]`表示第i天交易k次后持有/不持有股票的最大收益 --- ### **三、状态转移推导方法论** #### 步骤1:基础关系定位 **核心问题**:"当前状态可能从哪些前置状态转移而来?" - **正向推导**:从已知状态推导后续状态(常用) - **逆向推导**:当前状态由哪些前置状态决定 **案例——背包问题**: `dp[i][w] = max(不选第i件物品 → dp[i-1][w], 选第i件物品 → dp[i-1][w-wi] + vi)` #### 步骤2:数学关系式提炼 将步骤1的关系转化为数学表达式,注意: - 明确变量间的约束条件(如容量限制、步长限制) - 处理多决策路径取极值(max/min) **案例——最短路径**: `dp[x][y] = min(dp[x-1][y], dp[x][y-1]) + grid[x][y]` #### 步骤3:验证完备性 检查是否覆盖所有可能状态转移路径,常见问题: - 漏掉某些决策分支 - 未处理非法状态(如背包容量为负) --- ### **四、经典案例精析** #### 案例1:最长回文子序列 **状态定义**: `dp[i][j]`表示字符串s[i..j]的最长回文子序列长度 **转移方程推导**: ```math dp[i][j] = \begin{cases} dp[i+1][j-1] + 2 & \text{if } s[i]=s[j] \\ \max(dp[i+1][j], dp[i][j-1]) & \text{otherwise} \end{cases} ``` **推导逻辑**: - 首尾字符相同时,结果=中间子串解+2 - 不相同时,取舍弃左端或右端的最大值 #### 案例2:正则表达式匹配 **状态定义**: `dp[i][j]`表示s前i字符与p前j字符是否匹配 **转移方程**: ```math dp[i][j] = \begin{cases} dp[i-1][j-1] & \text{if } p[j-1] \neq '*' \text{且匹配} \\ (dp[i][j-2] \text{ or } (匹配且 dp[i-1][j])) & \text{if } p[j-1] = '*' \end{cases} ``` **关键点**: - 处理'*'的零次匹配(跳过pattern)和多次匹配(消耗字符串) --- ### **五、调试与优化技巧** 1. **可视化DP表** 打印二维表格检查状态转移是否符合预期 2. **边界条件验证** - 初始值设置(如dp[0][0]=true) - 索引越界防护(如i-1≥0时才允许访问) 3. **空间压缩策略** 滚动数组:当当前状态仅依赖前一行时,可将O(n^2)空间降为O(n) --- ### **六、高频易错点** 1. **状态定义冗余** → 通过问题重述精简状态维度 2. **转移条件不完整** → 绘制状态转移图查漏 3. **循环顺序错误** → 确保计算当前状态时前置状态已就绪 --- **总结**:状态转移方程的建立需要: 1. 深入理解问题本质 → 80%的难度在准确建模 2. 大量经典题型训练 → 培养状态抽象直觉 3. 严谨的数学验证 → 避免逻辑漏洞 建议从简单DP问题(斐波那契数列、爬楼梯)入手,逐步过渡到背包、LCS等经典模型,最后挑战状态机类复杂问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值