1.股票问题的引入
股票问题是动态规划问题中的经典场景问题,在腾讯的笔试题目当中也出现过,在第五题的位置,所以特地参考了大神的通用解题思路来系统的复习,其中也会涉及到状态机的概念,其实也就是买卖股票的过程中会出现几种特定的状态,买和卖作为一种操作策略会使得问题的状态从S1变为S2。当然在解题过程中也需要想着说能否和背包问题一样做一些空间的压缩,从二维到一维,从O(n)到O(1)。
这里给出参考的文章地址:
(1)国外股票问题高亮
(2)国内优化版本文章
2.通解的表述
问题的核心就是如何把股票问题抽象为熟悉的动态规划问题,这里也就是会引入一些符号来对于状态进行表示,通过研究当前时刻和之前时刻的状态的变化关系来建立状态转移方程。
首先引入一些符号的表征:
-
用 n 表示股票价格数组的长度;
-
用 i 表示第 i 天(i 的取值范围是 0 到 n - 1);
-
用 k 表示允许的最大交易次数;
-
用 T[i][k] 表示在第 i 天结束时,最多进行 k 次交易的情况下可以获得的最大收益。
-
T[i][k][0] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 0 份股票的情况下可以获得的最大收益;
-
T[i][k][1] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 1 份股票的情况下可以获得的最大收益。
建立基准问题和状态转移矩阵
基准情况
T[-1][k][0] = 0, T[-1][k][1] = -Infinity T[i][0][0] = 0, T[i][0][1] = -Infinity
状态转移矩阵
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i]) T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i])
核心的流程已经表达完毕,剩下来的就是在特定的场景下进行空间和时间复杂度上面的优化
-
3.特殊问题的求解
/**
* 买卖股票的最佳时机
* 给定一个数组 prices ,它的第i个元素prices[i]表示一支给定股票第 i 天的价格。
* 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
* 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
* @param prices
* @return
*/
public int maxProfit1(int[] prices) {
// 先试用一下通解的求法
int n = prices.length;
// 做基准条件的初始化
int profit0 = 0;
int profit1 = Integer.MIN_VALUE;
// 一般化状态的转移方程构建
for (int i = 1; i <= n; i++) {
profit0 = Math.max(profit1 + prices[i - 1], profit0);
profit1 = Math.max(- prices[i - 1], profit1);
}
return profit0;
}
/**
* 买卖股票的最佳时机 II
* 给你一个整数数组 prices ,其中prices[i] 表示某支股票第 i 天的价格。
* 在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
* 返回 你能获得的 最大 利润。
* @param prices
* @return
*/
public int maxProfit2(int[] prices) {
// 这里一步进入简化版本
int n = prices.length;
int profit0 = 0;
int profit1 = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
profit0 = Math.max(profit1 + prices[i - 1], profit0);
profit1 = Math.max(profit0 - prices[i - 1], profit1);
}
return profit0;
}
/**
* 买卖股票的最佳时机 III
* 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
* 设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔交易。
* 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
* @param prices
* @return
*/
public int maxProfit3(int[] prices) {
// 其实也就是四种状态
// s1 t[1][0] s2 t[1][1] s3 t[2][0] s4 t[2][1] 可以采用状态机的方式来处理
int n = prices.length;
int s1 = 0;
int s2 = Integer.MIN_VALUE;
int s3 = 0;
int s4 = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
s4 = Math.max(s1 - prices[i - 1], s4);
s1 = Math.max(s2 + prices[i - 1], s1);
s2 = Math.max(-prices[i - 1], s2);
s3 = Math.max(s4 + prices[i - 1], s3);
}
return Math.max(s1, s3);
}
/**
* 买卖股票的最佳时机 IV
* 给定一个整数数组prices ,它的第 i 个元素prices[i] 是一支给定的股票在第 i 天的价格。
* 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
* 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
* @param k
* @param prices
* @return
*/
public int maxProfit4(int k, int[] prices) {
int n = prices.length;
int tranLength = n / 2;
// 这里的k说的是无限次交易,但是其实也是有限制的,就是和股票交易的天数是高度绑定的
if (k >= tranLength) k = tranLength;
int[][] dp = new int[k + 1][2];
// 状态初始化
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int j = 0; j <= k; j++) {
dp[j][0] = 0;
dp[j][1] = Integer.MIN_VALUE;
}
// 一般情况迭代
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
dp[j][0] = Math.max(dp[j][0], dp[j][1] + prices[i - 1]);
dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - prices[i - 1]);
}
}
// 获取最大的交易数值
int ans = Integer.MIN_VALUE;
for (int i = 0; i <= k; i++) {
ans = Math.max(ans, dp[i][0]);
}
return ans;
}
/**
* 买卖股票的最佳时机含手续费
* 给定一个整数数组prices,其中 prices[i]表示第i天的股票价格 ;整数fee 代表了交易股票的手续费用。
* 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
* 返回获得利润的最大值。
* 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
* @param prices
* @param fee
* @return
*/
public int maxProfit5(int[] prices, int fee) {
// 这里一步进入简化版本
int n = prices.length;
int profit0 = 0;
int profit1 = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
profit0 = Math.max(profit1 + prices[i - 1], profit0);
profit1 = Math.max(profit0 - prices[i - 1]-fee, profit1);
}
return profit0;
}
/**
* 最佳买卖股票时机含冷冻期
* 给定一个整数数组prices,其中第prices[i]表示第i天的股票价格 。
* 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
* 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
* 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
* @param prices
* @return
*/
public int maxProfit6(int[] prices) {
// 盘点一下状态变量
// t[i][k][1] = t[i-2][k][0]-prices[i-1] , t[i-1][k][1]
// t[i][k][0] = t[i-1][k][1]+prices[i-1] , t[i-1][k][1]
int n = prices.length;
int s1 = 0;
int s2 = 0;
int pre = 0;
int s3 = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
s2 = Math.max(s2, s3 + prices[i - 1]);
s3 = Math.max(s3, s1 - prices[i - 1]);
// 对于s1进行动态更新
s1 = pre;
pre = s2;
}
return s2;
}
其实总结来说,面对股票问题或者说动态规划的问题,我们核心就在于要明确到所有的状态,状态之间的转移都是依托于买入和卖出的操作,定义好交易次数的变化(买入的时候加一,还是卖出的时候加一),定义好手续费的变化,结合通用的状态转移方程就可以解决所有的问题了。