赞
踩
算法-股票买卖问题 是比较典型的动态规划问题,这种类型的题目简而言之就是根据给定的股价数组按照一定的规则买入卖出,求最大的收益。笔者提到的这道题目在 leetcode原题 中有许多大佬给出了解法,其中一种如下:
相信绝大多数读者和笔者一样,乍一眼看到下面的代码时都会一头雾水,搞不明白这样写的逻辑是什么。即便勉强看明白了代码,换一道类似的题目还是做不出来,甚至连任何思路都没有。
那为什么别人能写出这么诡异的解法?因为这类问题是有框架的,本文主要来介绍下这个框架
public static int maxProfit_k_2(int[] prices) {
int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
for (int price : prices) {
dp_i20 = Math.max(dp_i20, dp_i21 + price);
dp_i21 = Math.max(dp_i21, dp_i10 - price);
dp_i10 = Math.max(dp_i10, dp_i11 + price);
dp_i11 = Math.max(dp_i11, -price);
}
return dp_i20;
}
首先确定一个思路,所有动态规划的问题都是可以利用状态
来进行穷举的。具体到股票买卖的每一天,可以看看总共有几种可能的状态
,再找出每个状态
对应的选择
,下面解释一下就很容易明白了
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
以股票买卖问题为例,每天都有三种选择
:买入、卖出、无操作,可以用 buy, sell, rest 表示。问题在于,并不是每天都可以任意选择这三种选择,因为必须先 buy 之后才能 sell,所以 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。另外还有交易次数 k 的限制,就是说 sell 还只能在 k > 0 的前提下操作
看上去很复杂,但是现在的目的只是穷举,再多的状态也能一次全部列举出来。这个问题中可以用三个变量描述一个状态
,第一个是天数,第二个是允许交易的最大次数,第三个是当前的股票持有状态(即之前说的 rest 的状态,可以用 1 表示持有,0 表示没有持有),然后可以用一个三维数组存储全部组合:
数组: dp[i][k][0 or 1], 0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数,此问题共 n × K × 2 种状态
用自然语言可以描述出每一个状态的含义:
dp[3][2][1]
的含义是:
今天是第 3 天,至今最多进行了 2 次交易,现在手上持有着股票dp[2][3][0]
的含义是
今天是第二天,至今最多进行了 3 次交易,现在手上没有持有股票
我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,手上没有持有股票时最多获得多少利润
状态
穷举完毕,需要考虑每种状态
有哪些选择
,应该如何更新状态
。从上文我们已经知道,基于持有股票的状态,可以有如下的状态转移:
1. 今天没有持有股票,有两种可能:
要么是昨天就没有持有,然后今天选择 rest,所以今天还是没有持有;
要么是昨天持有股票,但是今天 sell 了,所以今天没有持有股票了。
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
2. 今天持有股票,有两种可能:
要么昨天就持有着股票,然后今天选择 rest,所以今天还持有着股票;
要么昨天本没有持有,但今天选择 buy,所以今天就持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
以上解释很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i],今天的最大利润就是这两种选择中较大的那个。注意 k 的限制,在选择 buy 的时候,把 k 减小了 1,也可以在 sell 的时候减 1
我们已经完成了动态规划中最困难的一步:状态转移方程,不过现在还差最后一步,就是定义 base case,即最简单的情况:
1. 因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 dp[-1][k][0] = 0 2. 还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能 dp[-1][k][1] = -infinity 3. 因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 dp[i][0][0] = 0 4. 不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能 dp[i][0][1] = -infinity 总结一下 base case: dp[-1][k][0] = dp[i][0][0] = 0 dp[-1][k][1] = dp[i][0][1] = -infinity 状态转移方程: dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
当 k = 2 时,可以依据以上状态方程写出解法:
新状态只和相邻的一个状态有关,可以不用整个 dp 数组,只需一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1),也就是上文提到的大佬给出的解法
public static int maxProfit_k_2_dp(int[] prices) { int maxK = 2; int[][][] dp = new int[prices.length][maxK + 1][2]; for (int i = 0; i < prices.length; i++) { for (int k = 1; k <= maxK; k++) { if (i == 0) { dp[i][k][0] = 0; dp[i][k][1] = -prices[i]; continue; } dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i]); dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k-1][0] - prices[i]); } } return dp[prices.length - 1][maxK][0]; }
动态规划的关键就在于列举出所有可能的状态
,然后思考如何穷举更新这些状态
。一般用一个多维 dp 数组储存这些状态,从 base case 开始向后推进,推进到最后的状态,就是我们想要的答案
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。