状态机动态规划之股票问题

本文介绍了动态规划解决股票问题,包括卖股票的最佳时机 III 和 IV。在最佳时机 III 中,允许进行两笔交易,通过状态机分析得出动态规划状态转移方程,并给出代码实现。在最佳时机 IV 中,扩展到最多 k 笔交易,分析状态表示和状态转移方程,同样提供代码解法。动态规划中涉及多种状态变化,理解状态转移有助于解决这类问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

卖股票的最佳时机 III

题目

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例

示例1

输入:prices = [3,3,5,0,0,3,1,4]

输出:6

解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例2

输入:prices = [1,2,3,4,5]

输出:4

解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

状态表示数组和状态转移

在这道题目当中我们也是二维数组进行状态的表示,二维数组为dp[N][5],5表示我们有5个状态,dp[N][i]表示第N天的第i个状态能够多大的收益!(为了方便下面介绍,假设一天有一个股票,dp[N][]表示第N天的状态,对应第N个股票的状态)

  • dp[N][0],表示第N天一次买入和卖出的操作都没有过,那么dp[N][0] = dp[N - 1][0],跟前一天的状态一样,都没有进行股票的买入和卖出,其实也可以直接令dp[N][0] = 0,因为没有进行操作我们的收益肯定等于0。
  • dp[N][1],表示第N天已经进行过第一次买入,这个买入可以是在第N天进行买入,也可以在前面N-1天买入,然后在第N天保持状态。如果第N天刚刚进行买入,那么我们的收益就是从前一天一次买入和卖出都没有操作转移过来的,那么就有dp[N][0] - prices[i],因为根据上面的分析dp[N][0] = 0,那么直接让dp[N][1] = -prices[i]即可。如果在前N-1天已经进行了买入,那么在第N天就不行操作,即在第N天收入为0,即dp[N][1] = dp[N - 1][1]。
  • dp[N][2],表示第N天已经进行过第一次卖出,这个状态可以是在第N天进行卖出,也可以是在前面N-1天已经卖出,然后在第N天保持状态如果在第N天进行第一次卖出那么我们在第N天的收益就等于prices[i],再加上前N-1天买入一次的收益,即dp[N][2] = dp[N - 1][1] + prices[i]。如果前N-1天已经卖出,那么直接保持状态即可,我们在第N天的收益就为0,那么dp[N][2] = dp[N - 1][2]。
  • dp[N][3],表示第N天已经进行过第二次买入,这个状态可以是在第N天进行买入,也可以是在前面N-1天买入,然后在第N天保持状态。如果在第N天进行第二次买入那么我们在第N天的收益就等于-prices[i],再加上前N-1天买入卖出一次的收益,即dp[N][3] = dp[N - 1][2] - prices[i]。如果前N-1天已经有了第二次买入的操作,那么直接保持状态即可,我们在第N天的收益就为0,那么dp[N][3] = dp[N - 1][3]。
  • dp[N][4],表示第N天已经进行过第二次卖出,这个状态可以是在第N天进行买入,也可以是在前面N-1天卖出,然后在第N天保持状态。如果是在第N天卖出,那么在第N天的收益为prices[i],再加上前N-1天买入两次卖出一次的收益dp[N][3],那么dp[N][4] = dp[N - 1][3] + prices[i]。如果是前N-1天已经买入卖出两次了,那么直接保持前一天的状态即可,即dp[N][4] = dp[N-1][4]。

根据上面的分析我们可以得到下面的状态机(状态转移图):

相信看到这里你就应该能够理解为什么这种动态规划叫做状态机动态规划,因为在这种动态规划当中数据存在很多状态,而我们需要进行仔细的分析,分析清楚这里面的状态该如何进行转移,进而分析出来各种状态之间的转移关系,这种模式跟状态机非常像,因此叫做状态机动态规划

数据流依赖分析和状态转移方程

假如可以买卖股票的天数一共有N天,那么我们最终需要求出来的结果是dp[N][4],表示第N天已经买入卖出2次,将两次使用的机会都是用完了,为什么我们最终的结果是dp[N][4]呢?这你可能疑惑万一我买入一次卖出一次能够得到的收益最大呢?我们是允许在同一天多次买入和卖出股票的,而在同一天买入和卖出股票收益为0,所以不影响最后的结果,因此买入卖出一次最终也可以转移到买入卖出两次(其中一次在同一天买入和卖出即可,我们在对数组进行初始化的时候就需要进行多次买入和卖出(可以看下文当中对数组初始化的分析)),因此我们最终需要返回的结果就是dp[N][4]。

而根据上面的分析我们知道,从上图可以看出转移到dp[N][4]这个状态一共有两种方式,我们应该选择转移之后两者方式得到的价值比较大的那个,即dp[N][4] = max(dp[N - 1][4], dp[N - 1][3] + prices[i]);,而dp[N - 1][4]的转移又有两种方式我们也应该选择其中较大的,dp[N - 1][3]也有两种转移方式,因此其也应该选择两者当中比较大的那个值,即dp[N][3] = max(dp[N - 1][3], dp[N - 1][2] - prices[N]);,同理我们可以得到其他状态的转移方程,每个数据都是需要选择转移之后价值最大的那个,最终我们的状态转移方程如下:

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]);

动态规划设计

在求解动态规划问题的时候通常的步骤有以下几个:

  • 寻找能够表示状态的数组dp,即我们需要寻找dp的含义,分析需要用几纬数组表示具体的状态。
  • 通过分析问题,寻找动态转移公式。
  • 初始化状态数组。
  • 通过分析动态转移方程,确定数组的遍历顺序。

综合上面的分析,我们的初始化代码如下:

dp[0][0] = 0;dp[0][1] = -prices[0];dp[0][3] = 0;dp[0][3] = -prices[0];dp[0][4] = 0;

根据状态转移方程,我们知道第i天依赖于第i-1天的数据,因此我们遍历的顺序为从前到后进行遍历。

代码

class Solution {  public int maxProfit(int[] prices) {    int[][] dp = new int[prices.length][5];    // dp[i][0] 表示一次买入和卖出都没有    // dp[i][1] 表示第一次买入    // dp[i][2] 表示第一次卖出    // dp[i][3] 表示第二次买入    // dp[i][4] 表示第二次卖出    dp[0][1] = -prices[0];    dp[0][3] = -prices[0];    for (int i = 1; i < prices.length; i++) {      dp[i][0] = dp[i - 1][0];      dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);      dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);      dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);      dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);    }    return dp[prices.length - 1][4];    // 注意数据之前传递依赖的关系    // 因为要求 dp[N][4] 当中    // 最大的值 因此需要求解 dp[N - 1][4] 和 dp[i - 1][3] 的最大值    // ......  }}

上面的代码的时间和空间复杂度分别为O(n)和O(n)。

空间复杂度优化

其实我们可以使用一个单行数组进行优化,优化代码如下:

class Solution {  public int maxProfit(int[] prices) {    int[] dp = new int[5];    dp[1] = -prices[0];    dp[3] = -prices[0];    for (int i = 1; i < prices.length; i++) {      dp[0] = dp[0]; // 这一行可以不要的 放在这里只是为了状态转移方程的完整      dp[1] = Math.max(dp[1], dp[0] - prices[i]);      dp[2] = Math.max(dp[2], dp[1] + prices[i]);      dp[3] = Math.max(dp[3], dp[2] - prices[i]);      dp[4] = Math.max(dp[4], dp[3] + prices[i]);    }    return dp[4];  }}

我们现在来简要分析一下上面的代码为什么可行:

比如现在i=3,现在要进行更新,现在的dp数组还是i=2的状态,如果用二维数组来表示的话,现在的单行数组中的dp[i]相当于二维数组当中的数据dp[2][i],假如我们现在需要更新dp[3][2],根据二维数组的动态转移方程,我们需要二维数组第二行的数据dp[2][2],但是此时的单行数组当中的数据还没有更新,也就是说dp[2]等于 dp[2][2](前面的dp表示单行数组,后面的dp表表示二维数组的dp),因此还是上一个状态的数据,因此更新没有问题。

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]);

根据上面的状态转移方程我们知道dp[3][2]依赖于dp[2][1],而dp[2][1]相当于dp[1],但是在下面的代码当中,我们在更新dp[2]之前dp[1]已经更新了,也就是说dp[1]已经是第三行的状态了,即dp[1] = dp[3][1],而现在更新的时候需要的是第二行的状态,因此这就不对了。

class Solution {  public int maxProfit(int[] prices) {    int[] dp = new int[5];    dp[1] = -prices[0];    dp[3] = -prices[0];    for (int i = 1; i < prices.length; i++) {      dp[0] = dp[0]; // 这一行可以不要的 放在这里只是为了状态转移方程的完整      dp[1] = Math.max(dp[1], dp[0] - prices[i]);      dp[2] = Math.max(dp[2], dp[1] + prices[i]);      dp[3] = Math.max(dp[3], dp[2] - prices[i]);      dp[4] = Math.max(dp[4], dp[3] + prices[i]);    }    return dp[4];  }}

那为什么上面的代码又可行呢?

  • 如果dp[1]是从上一行的dp[1]转移而来,那么就是符合我们的想法的,dp[2]使用的还是上一个(第2行)状态的dp[1],因为本行状态的(第3行)dp[1]和第2行的dp[1]相等。
  • 如果dp[1]是从dp[0] - prices[3]转移过来的,那么在这条语句dp[2] = Math.max(dp[2], dp[1] + prices[3]);当中,如果选择的是dp[2]那么也没关系,因为他跟dp[1]没有关系。如果选择的是dp[1] + prices[3],那么也没关系因为dp[1]减去了prices[3],这一加一减相当于没有收益,这并不影响最后的结果,因为这一卖一买都是在今天完成的,而对最终结果产生影响的肯定是在前面已经买入的操作(比如第2行的dp[1]就表示在之前进行第一次买入),而不会是在今天的买入,理解这一点就可以理解上的代码了。
  • 其余代码的影响也是类似的,都可以通过一加一减低消掉,最终都不影响最后的结果。

通过上述的优化之后空间复杂度变为O(1)。

卖股票的最佳时机 IV

题目

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例

示例1

输入:k = 2, prices = [2,4,1]

输出:2

解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例2

输入:k = 2, prices = [3,2,6,5,0,3]

输出:7

解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

问题分析

这个问题和本文当中的第一个问题其实差不多,只不过上面的问题是最多完成两笔交易,而在这个问题当中是最多可以完成k笔交易,这个问题相当于上面问题的推广,我们再来分析一下上一道题目的动态转移公式:

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]);

上面的公式用一个公式表示就是:

dp[i][j]=max(dp[i−1][j],dp[i−1][j−1]±prices[i]);

现在我们将这个问题进行推广:

  • 状态表示

  • dp[i][0] 表示一次买入和卖出都没有。

  • dp[i][2 * k - 1] 表示第 k 次买入。根据上文的分析,这个地方类似,有两种状态可以转换成第k次买入这个状态。如果前i-1天已经有k次买入了,则保持前面的状态就行,即dp[i][2 * k - 1] = dp[i - 1][2 * k - 1]。如果前i-1天已经有k-1次买入和卖出了,那么就需要进行买入,即dp[i][2 * k - 1] = dp[i - 1][2 * k - 2]- prices[i] 。

  • dp[i][2 * k] 表示第 k 次卖出。同样的,也有两个状态可以转换成这个状态。如果前i-1天已经有k次卖出了,则保持前面的状态就行,即dp[i][2 * k] = dp[i - 1][2 * k]。如果前i-1天已经有k次买入,那么就需要进行买入,即dp[i][2 * k] = dp[i - 1][2 * k - 1] + prices[i]。

  • 根据上面的分析,那么状态转移方程如下(其中j是偶数):

  • dp[i][j - 1] = max(dp[i - 1][j - 1], dp[i - 1][j - 2] - prices[i]);dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);

  • 同理我们最终需要返回的结果就是dp[N][2 * k]。

  • 数组初始化

  • 根据我们的分析,在买入之前必须卖出,因此在第一行当中所有的买入状态的价值都是-pirces[0],所有的卖出状态的价值都是0,因为买入之后再卖出就相当于没有买卖一样。

代码

class Solution {  public int maxProfit(int k, int[] prices) {    if (prices == null || prices.length == 0)      return 0;    int m = 2 * k + 1;    int[][] dp = new int[prices.length][m];    // dp[i][0] 表示一次买入和卖出都没有    // dp[i][2 * k - 1] 表示第 k 次买入    // dp[i][2 * k] 表示第 k 次卖出    for (int i = 1; i < m; i += 2) {      dp[0][i] = -prices[0];    }    for (int i = 1; i < prices.length; i++) {      dp[i][0] = dp[i - 1][0];      for (int j = 2; j < m; j += 2) {        dp[i][j - 1] = Math.max(dp[i - 1][j - 1], dp[i - 1][j - 2] - prices[i]);        dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);       }    }    return dp[prices.length - 1][2 * k];    // 注意数据之前传递依赖的关系  } }

总结

在本篇文章当中主要给大家介绍了另外两种股票问题,在这两个股票问题当中都有许多的状态,状态之间的转化也比较复杂,在仔细分析上面两个问题的状态转化之后相信你已经能够理解状态机动态规划了,这种含有比较复杂的状态之间的变化就叫做状态机动态规划,这种问题一般分析起来还是比较复杂的。

 

<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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值