动态规划_股票问题

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];
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值