导言:java
股票买卖问题是 LeetCode 算法题当中的一个系列问题,主要的考查点就是动态规划,可是若是针对这里的每道题都去思考和总结,其实获得的解法不具备通常性,这篇文章想要作的就是针对这一系列问题提炼出一个通用解法算法
股票买卖这一类的问题,都是给一个输入数组,表示的是天天的股价,而且限定你手头不能存有多于 1 支股,也就是手上有股票的时候必须卖掉才能再继续卖,并且只能买一支,通常来讲有下面几种问法:数组
固然还有一些变种,例如每次买卖都有交易费,另外还有就是交易事后必需隔一天再继续买卖。bash
这类题目当中的头两题,就是只能交易一次,和能够交易无数次是能够根据常识来解决的,只能交易一次无非就是遍历数组记录当前通过的最小值,而后用当前值减去最小值去记录差价,去差价最大的便可。只能交易无数次也是遍历数组,只要当前的值 比以前的存在的值大,就累加差价,而且把当前遍历到的值设置成 “以前的值”,而后继续遍历下去。框架
这里主要考虑 k 次交易的状况,固然这里的框架稍做调整也是能够用到只能交易一次和能够交易无数次的题目中去的,最好的解决方式确定是动态规划,可是关键在于状态怎么定义,动态规划的方程怎么写。咱们能够思考得出题目当中的变量有如下几个:优化
其中 “当前可得到的最大利润” 就是咱们最后要求解的值,那么这么看来 DP 数组的值能够试着用来表示 “当前可得到的最大利润” ,而后接着看,因为给定了输入数组,“股票的价格” 是和 “第几天” 绑定在一块儿的,这样 DP 的状态实际上是由 “第几天”,“手头有无股票”,还有 “第几回交易” 来决定的,那咱们就能够获得 DP 的状态:spa
DP[i][j][0] -> 第 i 天,第 j 次交易,手头没有股票的最大利润
DP[i][j][1] -> 第 i 天,第 j 次交易,手头有股票的最大利润
复制代码
这样,咱们要求解的答案就是:code
Max(DP[n][0][0], DP[n][1][0], ..., DP[n][k][0])
复制代码
这里补充一点就是,手头有股票的状况确定不会是最后的答案,由于股票的价格都是正数,买股票是要花钱的,即 DP[i][j][0] - prices[a] < DP[i][j][0]
leetcode
另一个问题就是动归的递推方程怎么写,由于当前的 DP 状态只会和它以前的状态相关,并不会被后面的状态所影响,并且在思考的过程当中要有一个认识就是,以前的 DP 值全是局部的最优解,所以,咱们能够思考一个问题是 “当前的最大利润和前面哪些状态相关?”,而后你会发现每一个状态只会和它相邻的状态影响,也就是第 i 天的最大利润能够经过第 i - 1 天的最大利润求解,第 k 次交易的最大利润能够经过第 k - 1 次交易的最大利润求解,另外就是手头有无股票也是能够经过买入和卖出相互转化的,所以咱们能够得出递推方程以下:get
DP[i][k][0] = Max(DP[i - 1][k][0], DP[i - 1][k - 1][1] + a[i])
DP[i][k][1] = Max(DP[i - 1][k][1], DP[i - 1][k - 1][0] - a[i])
复制代码
有了 DP 的状态定义,和递推方程,剩下的工做就是写代码了,应该问题已经基本解决了。
LeetCode 121. Best Time to Buy and Sell Stock
这道题比较简单,套进咱们以前总结解法也是能够很好解决的,k = 1,这里 DP 数组开 2 是为了来表示没有交易和交易 1 次:
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int[][][] dp = new int[prices.length][2][2];
dp[0][0][0] = 0; dp[0][0][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][1] + prices[i]);
dp[i][0][1] = Math.max(dp[i - 1][0][1], dp[i - 1][0][0] - prices[i]);
}
return dp[prices.length - 1][1][0];
}
复制代码
LeetCode 122. Best Time to Buy and Sell Stock II
由于这题的交易次数不受限制,也就是当前进行买和卖都是能够的,不用考虑前面的 k - 1 次的状态,所以咱们就没必要开多一维数组
public int maxProfit(int[] prices) {
if ((prices == null) || (prices.length < 2)) {
return 0;
}
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
复制代码
LeetCode 123. Best Time to Buy and Sell Stock III
这道题符合咱们以前讲到的框架,其中 k = 2,所以根据 DP 状态中的交易次数,咱们开长度为 3 的 DP 数组,分别用来表示第 0 次交易,第 1 次交易,第 2 次交易,其中 dp[i][0][0] 是不用根据前面的状态来更新的,其值永远等于零,即 dp[0][0][0] = dp[1][0][0] = ... = dp[n - 1][0][0]
,还有 dp[i][2][1] 也是不须要考虑的,由于交易次数限定为最高 2 次,不可能存在第三次交易的状况
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
// dp[i][j][k] -> day, time, whether have stock or not
int[][][] dp = new int[prices.length][3][2];
dp[0][0][1] = -prices[0]; dp[0][1][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][0][1] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][0][1]);
dp[i][1][0] = Math.max(dp[i - 1][0][1] + prices[i], dp[i - 1][1][0]);
dp[i][1][1] = Math.max(dp[i - 1][1][0] - prices[i], dp[i - 1][1][1]);
dp[i][2][0] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][2][0]);
}
return dp[prices.length - 1][2][0];
}
复制代码
LeetCode 188. Best Time to Buy and Sell Stock IV
这题就是咱们以前讨论当中的案例,可是考虑到第 0 次交易的状况是无法根据前面的 DP 的值来计算的,为来计算的方便,把第 0 次交易的状况单独挪出第二层循环进行处理,还有就是,LeetCode 给了一个很是极端的 testcase,就是 k 很是大,k >> prices.length
,为了程序可以顺利经过,这种状况下就直接变成第二题的解法,这里就直接按常识简写了:
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
if (k > prices.length / 2) {
int max = 0; int hold = prices[0];
for (int i = 1; i < prices.length; ++i) {
max += Math.max(0, prices[i] - prices[i - 1]);
}
return max;
}
// dp[i][j][0] -> at ith day, jth transaction, without stock in hand
// dp[i][j][1] -> at ith day, jth transaction, with stock in hand
int[][][] dp = new int[prices.length][k + 1][2];
// init
for (int i = 0; i <= k; ++i) {
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < prices.length; ++i) {
dp[i][0][1] = Math.max(dp[i - 1][0][1], dp[i - 1][0][0] - prices[i]);
for (int j = 1; j <= k; ++j) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j - 1][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j][0] - prices[i]);
}
}
return dp[prices.length - 1][k][0];
}
复制代码
LeetCode 309. Best Time to Buy and Sell Stock with Cooldown
这道题在买卖不受限制题目的基础上,加了条件,就是买卖后,必须等上至少一天才能继续买卖,这样的话状态略微改变便可,就是在以前 “手头有股票” 和 “手头没有股票” 两种状态的基础上,多加一个 “冷却” 这么一个状态;递推方程跟以前不一样的是,买股票的话,只能从前一天的 “冷却” 状态来决定,而不是 “手头没有股票”
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int[][] dp = new int[prices.length][3];
dp[0][0] = 0; dp[0][1] = -prices[0]; dp[0][2] = 0;
for (int i = 1; i < prices.length; ++i) {
dp[i][0] = Math.max(dp[i - 1][1] + prices[i], Math.max(dp[i - 1][0], dp[i - 1][2]));
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][0], dp[i - 1][2]);
}
return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][2]);
}
复制代码
LeetCode 714. Best Time to Buy and Sell Stock with Transaction Fee
这道题也是在买卖不受限制题目的基础上加上了 “每次交易都要交费用,每次费用相同” 这么一个条件,那其实在买卖不受限制题目的基础上,惟一须要改变的就是在卖票的时候减去交易费用便可,固然在买股票的时候减去这个交易费也是能够的
public int maxProfit(int[] prices, int fee) {
if (prices == null || prices.length < 2) {
return 0;
}
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
复制代码
以上六道题就是 LeetCode 当中股票系列的所有内容,固然这样的设定 DP 状态和定义 DP 递推方程的思想,是能够复用到其余类型的 DP 问题当中去的,总的来讲就是根据变量来定义 DP 数组当中存的值以及状态,根据当前状态和以前状态的关系来肯定 DP 方程,这个须要平时的积累和大量的刷题练习。另外题目解答当中没有提到的是,对于 DP 数组的空间优化,咱们能够利用滚动数组来优化。