原题链接
我们先来看下最简单的股票问题。给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
我们从题目所给信息可以得到;
那么就可以用贪心的思想,在遍历股票价格的同时。我们遍历到一个价格,就假设当天把他卖出,同时维护历史最低价,然后计算差值就是最大利润。代码如下:
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int res = 0;
for (int price : prices) {
// 找到前几天的最小值
minPrice = Math.min(minPrice, price);
// 我们求得:如果卖当前股票,那么当前股票的 最大利润 = 当前价格 - 历史最小价
res = Math.max(res, price - minPrice);
}
return res;
}
原题链接
在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有 一 股票。你也可以先购买,然后在同一天出售。也就是在第一题的基础上,可以选择买卖多次。
其实这样反而比第一题要简单:
代码如下:
public int maxProfit(int[] prices) {
int res = 0;
for (int i = 0; i < prices.length - 1; i++) {
int cost = prices[i + 1] - prices[i];
// 只有涨的价格,才有利润。如果是亏的,不计入
if (cost > 0) {
res += cost;
}
}
return res;
}
在第二题的基础上,改变了限制:股票最多只能买两次。
这题目可就不能跟上面两道题一样了,我们需要开始用变量代表各个状态来完成价格的计算了。也就是动态规划。这里面用到的状态有三种:
那么我们用三维数组来表达:dp[i][j][r]
,代表当前天数的最大利润。
那么就有
int len = prices.length;
int[][][] dp = new int[len][2][3];
// 第一天,未持股,没卖过
dp[0][0][0] = 0;
// 第一天,买入一只股票,没卖过。
dp[0][1][0] = -prices[0];
// 第一天不可能卖过,因此凡是r = 1或者2的全部是最小值。
dp[0][0][1] = dp[0][0][2] = dp[0][1][1] = dp[0][1][2] = min;
那么接下来需要开始分析各个状态情况下公式的推导:
状态 | 利润 |
---|---|
今日未持股,卖出过0次 dp[i][0][0] | 都没买过,利润自然是0,无论哪一天: dp[i][0][0] = 0 |
今日未持股,卖出过1次 dp[i][0][1] | dp[i][0][1] = Max(dp[i-1][1][0]+price[i](今日卖出),dp[i-1][0][1](昨日未持股)) |
今日未持股,卖出过2次 dp[i][0][2] | dp[i][0][2] = Max(dp[i-1][1][1]+price[i](今日卖出),dp[i-1][0][2](昨日未持股)) |
持股,卖出过0次 dp[i][1][0] | dp[i][1][0] = Max(dp[i-1][0][0] - price[i](昨日买入了),dp[i-1][1][0](前几天就买过了)) |
持股,卖出过1次 dp[i][1][1] | dp[i][1][1] = Max(dp[i-1][0][1] - price[i](昨日买入了),dp[i-1][1][1](前几天就买过了)) |
持股,卖出过2次 dp[i][1][2] | 最多卖两次,不能再卖下去,这种情况不存在:dp[i][1][02 = Integer.MIN |
那么最后股票的最大利润就有两种情况:
dp[len-1][0][1]
。dp[len-1][0][2]
。那么最终代码如下:
public int maxProfit(int[] prices) {
int[][][] dp = new int[prices.length][2][3];
int min = Integer.MIN_VALUE / 2;
// 第一天,未持股,没卖过
dp[0][0][0] = 0;
// 第一天,买入一只股票,没卖过。
dp[0][1][0] = -prices[0];
// 第一天不可能卖过,因此凡是r = 1或者2的全部是最小值。
dp[0][0][1] = dp[0][0][2] = dp[0][1][1] = dp[0][1][2] = min;
// 第一天相关的值已经初始化了,从第二天开始计算
for (int i = 1; i < prices.length; i++) {
// 套公式 ,当前未持股
dp[i][0][0] = 0;
dp[i][0][1] = Math.max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
dp[i][0][2] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
// 持股
dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
dp[i][1][1] = Math.max(dp[i - 1][0][1] - prices[i], dp[i - 1][1][1]);
dp[i][1][2] = min;
}
return Math.max(0, Math.max(dp[prices.length - 1][0][1], dp[prices.length - 1][0][2]));
}
这题的难度其实并没有上升太多,因为在第三题里面,已经把状态的转移公式写的很清楚了。只不过是买卖的次数由2次变更为k
次罢了。
那么代码的不同无非就是两个:
dp
数组的时候,2需要改成k+1
。for
循环中,需要在增加一个for
循环。用来遍历k
的下标数组。因为第三题中,k
只有0,1两种可能,因此没写成for
循环。其中的公式更改为下
// 如果是持股 昨天买入(因此昨天未持股) 前几天就持股了
dp[i][1][k] = max(dp[i-1][0][k] - price , dp[i-1][1][k])
// 如果是未持股, 昨天卖出(因此昨天持股) 。 前几天也未持股
dp[i][0][k] = max(dp[i-1][1][k-1] + price , dp[i-1][0][k])
代码如下:
// 第一天相关的值已经初始化了,从第二天开始计算
for (int i = 1; i < prices.length; i++) {
for (int k = 1; k <= K; k++) {
// 如果是持股 昨天买入(因此昨天未持股) 前几天就持股了
dp[i][1][k] = Math.max(dp[i - 1][0][k] - prices[i], dp[i - 1][1][k]);
// 如果是未持股, 昨天卖出(因此昨天持股) 。 前几天也未持股
dp[i][0][k] = Math.max(dp[i - 1][1][k - 1] + prices[i], dp[i - 1][0][k]);
}
}
那么初始化相关的代码我们就要做(遍历r
位):
k
等于多少,都赋值为-prices[0]
。for (int k = 0; k <= K; k++) {
dp[0][0][k] = 0;// 第一天不持股,k不重要
dp[0][1][k] = -prices[0];// 第一天持股
}
如果是后面几天(遍历i
位):
Max(昨天买入,前几天就持有)
的最大值。for (int i = 1; i < prices.length; i++) {
dp[i][0][0] = 0;
dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
}
最终代码就是:
public int maxProfit(int K, int[] prices) {
int[][][] dp = new int[prices.length][2][K + 1];
for (int k = 0; k <= K; k++) {
dp[0][0][k] = 0;// 第一天不持股,k不重要
dp[0][1][k] = -prices[0];// 第一天持股
}
// 第i天如果买入股票,没卖过,未持股
for (int i = 1; i < prices.length; i++) {
dp[i][0][0] = 0;
dp[i][1][0] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][1][0]);
}
// 第一天相关的值已经初始化了,从第二天开始计算
for (int i = 1; i < prices.length; i++) {
for (int k = 1; k <= K; k++) {
// 如果是持股 昨天买入(因此昨天未持股) 前几天就持股了
dp[i][1][k] = Math.max(dp[i - 1][0][k] - prices[i], dp[i - 1][1][k]);
// 如果是未持股, 昨天卖出(因此昨天持股) 。 前几天也未持股
dp[i][0][k] = Math.max(dp[i - 1][1][k - 1] + prices[i], dp[i - 1][0][k]);
}
}
return Math.max(0, Math.max(dp[prices.length - 1][0][K], dp[prices.length - 1][0][K - 1]));
}
原题链接
总结就是:
如果有冷冻期,那么我们可以把股票的持有状态分为以下三种:我们用dp[i][j]
来代表第i
天的最大利润。j
代表对应的状态。
dp[i][0]
。dp[i][1]
。dp[i][2]
。先说第一种,持有股票的状态怎么来:
dp[i][0] = Math.max(dp[i-1][2] - price[i] , dp[i-1][0]);
再说下未持有+冷冻期的状态怎么来,只有一种情况:
dp[i][1] = dp[i-1][0] + price[i]
未持有+ 非冷冻期的状态:
dp[i][2] = Math.max(dp[i-1][1] , dp[i-1][2]);
初始化即第一天买入:
dp[0][0] = -price[0]
结果:
public int maxProfit2(int[] prices) {
int len = prices.length;
// 0:持股。1:未持股+冷冻。2:未持股+非冷冻
int[][] dp = new int[len][3];
// 持股
dp[0][0] = -prices[0];
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][0]);
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
}
// 最终肯定是未持股的状态,可能处于冷冻期,也可能处于非冷冻期
return Math.max(dp[len - 1][1], dp[len - 1][2]);
}
给定一个整数数组 prices
,其中 prices[i]
表示第 i
天的股票价格 ;整数 fee
代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
这个题目我们可以定义:
dp[i][0]
:第i
天未持有股票的最大利润。dp[i][1]
:第i
天持有股票的最大利润。那么推导公式如下:
// 今日不持有 = max (昨日卖出 ,和昨日不持有)
dp[i][0] = Math.max(dp[i-1][1]+prices[i]-fee,dp[i-1][0]);
// 今日持有 = max (昨日买入 ,和昨日持有)
dp[i][1] = Math.max(dp[i-1][0]-prices[i],dp[i-1][1]);
初始值:
dp[0][0] = 0
dp[0][1] = -prices[0]
最终结果:
public int maxProfit3(int[] prices, int fee) {
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < len; i++) {
// 今日不持有 = max (昨日卖出 ,和昨日不持有)
dp[i][0] = Math.max(dp[i - 1][1] + prices[i] - fee, dp[i - 1][0]);
// 今日持有 = max (昨日买入 ,和昨日持有)
dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
}
// 返回最终结果,一定是不持有状态
return dp[len - 1][0];
}
dp[i][j][r]
:第i
天,j
代表是否持有:0,1。r
代表卖过几次:0,1,2。k
次):同样三维数组dp[i][j][r]
:第i
天,j
代表是否持有:0,1。r
代表卖过几次:0,1,2…k。dp[i][j]
,第i
天,j
用三个数字代表三个状态。持有、未持有+冷冻期、未持有+非冷冻期。dp[i][j]
,第i
天,j
用两个数字代表两个状态:持有、未持有。