文章目录
1.题目详述
题目链接:风口的猪-中国牛市
2.解法一:暴力破解
思路:遍历所有的情况,得到最大的利润。
程序测试
class Solution {
private int[] prices;
public int maxProfit(int[] prices) {
if(prices.length<=1) return 0;
this.prices=prices;
return dfs(0,false,0,0);
}
private int dfs(int i,boolean isBuy,int buyTime,int count){
//如果已经买了两次了并且第二次已经卖掉了,或者已经遍历到最后,则停止遍历。
if((buyTime>=2 && !isBuy) || i>=prices.length) return count;
int currentPrices=prices[i];
if(isBuy){
//手上有股票,所以只能卖或者保持不动
//卖股票的情况
int sellCount=dfs(i+1,false,buyTime,count+currentPrices);
//保持不动的情况
int notSell=dfs(i+1,true,buyTime,count);
//计算是卖股票的情况与保持不动的利润比较,获取大的
count=Math.max(sellCount,notSell);
}else{
//手上没有股票,所以只能买或者保持不动
//1、买股票的情况
int buyCount=dfs(i+1,true,buyTime+1,count-currentPrices);
//2、保持不动的情况
int notBuy=dfs(i+1,false,buyTime,count);
//比较两种情况的利润,获取最大的利润
count=Math.max(buyCount,notBuy);
}
return count;
}
}
3.解法二:动态规划
3.1思路一
public class Solution {
public static int calculateMax(int[] prices) {
int firstBuy = Integer.MAX_VALUE; // 第一次买入最好的价格,越低越好
int firstSell = 0; // 第一次卖出后的最高收益,越高越好
int secondBuy = Integer.MIN_VALUE; // 第二次买入时还剩余的最高收益,越高越好
int secondSell = 0; // 第二次卖出时总的最高收益,越高越好
for (int price : prices) {
// 当前价格下第一次买入的价格
firstBuy = Math.min(firstBuy, price);
// 当前价格 - 买入价格 就是当前价格下第一次买卖的收益
firstSell = Math.max(firstSell, price - firstBuy);
// 第一次卖出的收益 - 当前价格,即当前价格下,第二次买入后还剩余收益
secondBuy = Math.max(secondBuy, firstSell - price);
// 剩余收益 + 当前价格,即当前价格下,第二次买卖的收益
secondSell = Math.max(secondSell, secondBuy + price);
}
return secondSell;
}
}
3.1思路二
思路:通过三维数组来表示三种不同的状态,第几天,最多交易的次数,以及当前是否股票。
题目分析:
1.如果当天持有股票,则有两个可能 dp[i][k][1]
- 昨天就有股票,今天不卖出保持原状,则最大利润跟昨天一样。
dp[i-1][k][1]
- 昨天没有股票,今天买入股票,则最大利润等于昨天的最大利润-今天买入的价格。
dp[i-1][k-1][0]
- 今天持有股票的最大利润取上面两者最大的值。
2.如果当天不持有股票,则有两个可能 dp[i][k][0]
- 昨天没有股票,今天也不买入,所以今天最大利润就等于昨天的最大利润。
dp[i-1][k][0]
- 昨天有股票,今天卖出,所以今天的最大利润就等于昨天的最大利润+今天买股票的钱。
dp[i-1][k][1]
- 今天不持有股票的最大利润取上面两者最大的值。
程序测试:
class Solution1 {
public int maxProfit(int[] prices) {
if(prices.length<=1) {
return 0;
}
int length=prices.length;
int k=2;//最多只能买卖两次
//一维的下标是代表第几天,二维的下标是代表最多交易次数,
//三维的下标代表当前手上是否持有股票,0未持有,1持有.
//对应的值为当前最大利润.
int dp[][][]=new int[length][k+1][2];
//第一天
dp[0][2][0]=0;//未买股票。
dp[0][2][1]=-prices[0];//
dp[0][1][0]=0;//不可能发生
dp[0][1][1]=-prices[0];//买了股票
for(int i=1;i<length;i++){
for(int j=k;j>0;j--){
//从第二天开始,有两种情况。
//如果想今天持有股票,那么有两种情况
//1、前面一天已经买了股票,今天持有,利润不变
int hold=dp[i-1][j][1];
//2、前面一天没有持有股票,今天买了股票.
int buy=dp[i-1][j-1][0]-prices[i];
dp[i][j][1]=Math.max(hold,buy);
//如果今天没有股票,那么也有两种情况
//1、前一天就没有股票,今天也不买
int notBuy=dp[i-1][j][0];
//2、前一天有股票,但是今天卖了。
int sell=dp[i-1][j][1]+prices[i];
dp[i][j][0]=Math.max(notBuy,sell);
}
}
return dp[length-1][k][0];
}
}
/*
优化代码结构,我们发现当天的最大利润,只跟前面一天的最大利润有关。
所以我们不需要数组存储每天的利润,最需要存储前一天的最大利润即可。
*/
class Solution2 {
public int maxProfit(int[] prices) {
if(prices.length<=1) return 0;
int length=prices.length;
int buyOneHaveStock=-prices[0];//最多买一次,并且有股票
int buyOneNotStock=0;//最多买一次,手上没有股票
int buyTwoHaveStock=-prices[0];//最多买两次,并且有股票
int buyTwoNotStock=0;//最多买两次,手上没有股票
for(int i=1;i<length;i++){
//今天最多买一次,手上没有股票,有两种情况
//要么昨天手上就没有股票,要么今天卖出了,卖出时需要加上当前股票价格。取两者最大值
buyOneNotStock=Math.max(buyOneNotStock,buyOneHaveStock+prices[i]);
//今天手上有股票
//要么昨天手上就有股票,今天持有,要么就是昨天没有股票,今天买入.
//-prices[i]解释:因为今天是最多买一次,那么今天买入的话,前面几天都不能买,也就是总利润为0.
buyOneHaveStock=Math.max(buyOneHaveStock,-prices[i]);
buyTwoNotStock=Math.max(buyTwoNotStock,buyTwoHaveStock+prices[i]);
//buyOneNotStock-prices[i]解释:因为今天做多买两次,且今天要买1次,那么前面些天最多只能买1次,所以前面些天的利润为buyOneNotStock-prices[i]
buyTwoHaveStock=Math.max(buyTwoHaveStock,buyOneNotStock-prices[i]);
}
return buyTwoNotStock;
}
}
4.补充_股票问题分析方法
作者:labuladong
链接:一个通用方法团灭 6 道股票问题
大佬写的非常详细!!!强烈推荐大家去看看
4.1分析股票问题的共同点
第一题是只进行一次交易,相当于 k = 1;第二题是不限交易次数,相当于 k = +infinity(正无穷);第三题是只进行 2 次交易,相当于 k = 2;剩下两道也是不限次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。
4.2穷举框架
在这里,我们不用递归思想进行穷举,而是利用「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。
针对股票问题,每天都有三种「选择」:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。
这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
我们想求的最终答案是 dp[n - 1][K][0]
,即最后一天,最多允许 K 次交易,最多获得多少利润。
为什么不是 dp[n - 1][K][1]?
因为 [1] 代表手上还持有股票,[0]表示手上的股票已经卖出去了,很显然后者得到的利润一定大于前者。
4.3状态转移框架
//第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
1.我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有
2.我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
1.我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票
2.我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了
如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
把上面的状态转移方程总结一下:
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])
4.4题目练习
4.4.1买卖股票的最佳时机
题目链接:买卖股票的最佳时机
k = 1
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i])
= max(dp[i-1][1][1], -prices[i])
解释:dp[i-1][0][0] = 0。
现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
直接写出代码:
int n = prices.length;
int[][] dp = new int[n][2];
for (int i = 0; i < n; 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], -prices[i]);
}
return dp[n - 1][0];
显然 i = 0 时 dp[i-1] 是不合法的。
可以这样处理:
for (int i = 0; i < n; i++) {
if (i - 1 == -1) {
dp[i][0] = 0;
// 解释:
// dp[i][0] = max(dp[-1][0], dp[-1][1] + prices[i])
// = max(0, -infinity + prices[i]) = 0
dp[i][1] = -prices[i];
//解释:
// dp[i][1] = max(dp[-1][1], dp[-1][0] - prices[i])
// = max(-infinity, 0 - prices[i])
// = -prices[i]
continue;
}
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];
新状态只和相邻的一个状态有关,其实不用整个 dp 数组,只需要一个变量储存相邻的那个状态就足够了,这样可以把空间复杂度降到 O(1):
int maxProfit_k_1(int[] prices) {
int n = prices.length;
// base case: dp[-1][0] = 0, dp[-1][1] = -infinity
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
// dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
// dp[i][1] = max(dp[i-1][1], -prices[i])
dp_i_1 = Math.max(dp_i_1, -prices[i]);
}
return dp_i_0;
}
4.4.2买卖股票的最佳时机 II
题目链接:买卖股票的最佳时机 II
k = +infinity,如果 k 为正无穷,那么就可以认为 k 和 k - 1 是一样的。可以这样改写框架:
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])
= max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])
我们发现数组中的 k 已经不会改变了,也就是说不需要记录 k 这个状态了:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
直接翻译成代码:
int maxProfit_k_inf(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}
return dp_i_0;
}
4.4.3买卖股票的最佳时机 III
题目链接:买卖股票的最佳时机 III
k = +infinity with cooldown,每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
翻译成代码:
int maxProfit_with_cool(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
int dp_pre_0 = 0; // 代表 dp[i-2][0]
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
dp_pre_0 = temp;
}
return dp_i_0;
}
4.4.4买卖股票的最佳时机 IV
题目链接:买卖股票的最佳时机 IV
k = +infinity with fee,每次交易要支付手续费,只要把手续费从利润中减去即可。改写方程:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
直接翻译成代码:
int maxProfit_with_fee(int[] prices, int fee) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);
}
return dp_i_0;
}
4.4.5最佳买卖股票时机含冷冻期
题目链接:最佳买卖股票时机含冷冻期
k = 2,k = 2 和前面题目的情况稍微不同,因为上面的情况都和 k 的关系不太大。要么 k 是正无穷,状态转移和 k 没关系了;要么 k = 1,跟 k = 0 这个 base case 挨得近,最后也没有存在感。
这道题 k = 2 和后面要讲的 k 是任意正整数的情况中,对 k 的处理就凸显出来了。我们直接写代码,边写边分析原因。
原始的动态转移方程,没有可化简的地方
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])
按照之前的代码,我们可能想当然这样写代码(错误的):
int k = 2;
int[][][] dp = new int[n][k + 1][2];
for (int i = 0; i < n; i++)
if (i - 1 == -1) { /* 处理一下 特殊值*/ }
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[n - 1][k][0];
为什么错误?
「穷举框架」:必须穷举所有状态。其实我们之前的解法,都在穷举所有状态,只是之前的题目中 k 都被化简掉了。这道题由于没有消掉 k 的影响,所以必须要对 k 进行穷举:
int max_k = 2;
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++) {
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) { /*处理 特殊值*/ }
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]);
}
}
// 穷举了 n × max_k × 2 个状态,正确。
return dp[n - 1][max_k][0];
这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况手动列举出来也可以:
dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], -prices[i])
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;
}
4.4.6买卖股票时机含手续费
题目链接:买卖股票时机含手续费
k = any integer,出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k 最多有多大呢?
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。
直接把之前的代码重用:
int maxProfit_k_any(int max_k, int[] prices) {
int n = prices.length;
if (max_k > n / 2)
return maxProfit_k_inf(prices);
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++)
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) { /* 处理 base case */ }
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]);
}
return dp[n - 1][max_k][0];
}