动态规划——股票问题

动态规划专题——股票问题

动态规划——股票问题_第1张图片

虽然前两题用贪心可以做,但是本篇文章全部使用动态规划来解决问题,比较具有体系。

121. 买卖股票的最佳时机

动态规划——股票问题_第2张图片

动态规划——股票问题_第3张图片 

动态规划——股票问题_第4张图片 

动规五部曲:

 第一步:确定dp数组(dp table)以及下标的含义

dp[i][0] 表示第i天持有股票所得最多现金 

dp[i][1] 表示第i天不持有股票所得最多现金

第二步:确定递推公式

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • 第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]

那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);

如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来

  • 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]

同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

第三步:dp数组如何初始化

由递推公式 dp[i][0] = max(dp[i - 1][0], -prices[i]); 和 dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);可以看出其基础都是要从dp[0][0]和dp[0][1]推导出来。

那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] = -prices[0];

dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;

第四步:确定遍历顺序

从递推公式可以看出dp[i]都是有dp[i - 1]推导出来的,那么一定是从前向后遍历。

第五步:举例推导dp数组

在此就不举例了

代码如下:

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        for(int i = 1 ; i < prices.length ; i++){
            dp[i][0] = Math.max(dp[i-1][0],-prices[i]);
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
        }
        return dp[prices.length-1][1];
    }
}

122. 买卖股票的最佳时机 II 

动态规划——股票问题_第5张图片

动态规划——股票问题_第6张图片 

 动态规划——股票问题_第7张图片

分析: 

从题意上面来看,股票问题Ⅱ 和 股票问题Ⅰ的区别在于我们可以多次买卖股票

和Ⅰ相比只有递推公式的区别

所以我们重点讲一讲递推公式。

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]

在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来

  • 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]

代码如下:

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = -prices[0];
        dp[0][1] = 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][1];
    }
}

123. 买卖股票的最佳时机 III

动态规划——股票问题_第8张图片

动态规划——股票问题_第9张图片 

 动态规划——股票问题_第10张图片

思路:和股票Ⅰ和股票Ⅱ相比,限制了买卖次数,必须为两次,所以dp数组和动态转移方程就会有变化。

第一步:确定dp数组以及下标的含义

一天一共就有五个状态,

  1. 没有操作
  2. 第一次买入
  3. 第一次卖出
  4. 第二次买入
  5. 第二次卖出

dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。

第二步:确定递推公式

需要注意:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区

达到dp[i][1]状态,有两个具体操作:

  • 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
  • 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]

那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][1]呢?

一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);

同理dp[i][2]也有两个操作:

  • 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
  • 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]

所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])

同理可推出剩下状态部分:

dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);

dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);

第三步:dp数组如何初始化

第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;

第0天做第一次买入的操作,dp[0][1] = -prices[0];

第0天做第一次卖出的操作,这个初始值应该是多少呢?

首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0,

从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。

所以dp[0][2] = 0;

第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?

第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。

所以第二次买入操作,初始化为:dp[0][3] = -prices[0];

同理第二次卖出初始化dp[0][4] = 0;

第四步:确定遍历顺序

从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。

第五步:举例推导dp数组

 省略

代码如下:

class Solution {
    public int maxProfit(int[] prices) {
        int dp[][] = new int[prices.length][5];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        dp[0][4] = 0;
        for(int i = 1 ; i < prices.length ; i++){
            dp[i][0] = 0;
            dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
            dp[i][2] = Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
            dp[i][3] = Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
            dp[i][4] = Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
        }
        return dp[prices.length-1][4];
    }
}

188. 买卖股票的最佳时机 IV 

动态规划——股票问题_第11张图片

动态规划——股票问题_第12张图片

动态规划——股票问题_第13张图片 

分析:如果认真看过上面第三题,会发现其实就是把2次变成了k次,这个时候我们开二维数组的大小就应该从 5 变成 2*k + 1 因为每次都涉及一次买入和卖出。所以具体就是给股票Ⅲ的初始化操作和递推加上for循环,直接看代码即可。 

 代码如下:

class Solution {
    public int maxProfit(int k, int[] prices) {
        if(prices.length <= 1){
            return 0;
        }
        int[][] dp = new int[prices.length][2*k+1];
        int[][] a = new int[1][2];
        dp[0][0] = 0;
        for(int i = 1 ; i < 2*k+1 ; i++){
            if(i % 2 == 1){
                dp[0][i] = -prices[0];
            }else{
                dp[0][i] = 0;
            }
        }

        for(int i = 1 ; i < prices.length ; i++){
            for(int j = 1 ; j < 2*k+1 ; j++){
                if(j % 2 == 1){
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
                }
            }
        }
        return dp[prices.length-1][2*k];
    }
}

309. 最佳买卖股票时机含冷冻期

动态规划——股票问题_第14张图片

动态规划——股票问题_第15张图片

动态规划——股票问题_第16张图片 

分析:这道题我感觉应该是一道困难题,因为不太好分析各个状态,加上冷冻期后。

 

第一步:确定dp数组以及下标的含义

dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。

  • 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
  • 卖出股票状态,这里就有两种卖出股票状态
    • 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态(手上没有持有股票,并且还不是当天卖出的)
    • 状态三:今天卖出了股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

j的状态为:

  • 0:状态一
  • 1:状态二
  • 2:状态三
  • 3:状态四

注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态

第二步:确定递推公式

达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:

  • 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
  • 操作二:今天买入了,有两种情况
    • 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
    • 前一天是保持卖出股票状态(状态二),dp[i - 1][1] - prices[i]

所以操作二取最大值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i]

那么dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);

达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:

  • 操作一:前一天就是状态二
  • 操作二:前一天是冷冻期(状态四)

dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:

  • 操作一:昨天一定是买入股票状态(状态一),今天卖出

即:dp[i][2] = dp[i - 1][0] + prices[i];

达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:

  • 操作一:昨天卖出了股票(状态三)

dp[i][3] = dp[i - 1][2];

第三步:dp数组如何初始化

这里主要讨论一下第0天如何初始化。

如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],买入股票所剩现金为负数。

保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行,

今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。

同理dp[0][3]也初始为0。

第四步:确定遍历顺序

从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。

第五步:手动模拟

免了免了,哈哈

代码如下:

class Solution {
    public int maxProfit(int[] prices) {
        int dp[][] = new int[prices.length][4];
        // 0 买入 1 两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态 2 今天卖出股票 3 冷冻期
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = 0;
        dp[0][3] = 0;
        for(int i = 1 ; i < prices.length ; i++){
            dp[i][0] = Math.max(Math.max(dp[i-1][3]-prices[i],dp[i-1][0]),(dp[i-1][1]-prices[i]));
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][3]);
            dp[i][2] = dp[i-1][0]+prices[i];
            dp[i][3] = dp[i-1][2];
        }
        return Math.max(Math.max(dp[prices.length-1][1],dp[prices.length-1][2]),dp[prices.length-1][3]);
    }
}

714. 买卖股票的最佳时机含手续费

动态规划——股票问题_第17张图片

动态规划——股票问题_第18张图片 

分析:此题与股票Ⅱ相比,仅仅是加了一个手续费,所以仅仅需要在股票Ⅱ的递推公式上,买入时 总金额减股票价格的同时再减去一个手续费即可。

代码如下:

 

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = -prices[0]-fee;
        dp[0][1] = 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][1];
    }
}

总结:股票问题还是蛮恶心的,理解起来,就算当天学会了,之后还会忘,还是需要多反复,希望对大家有帮助。

你可能感兴趣的:(动态规划专题,动态规划,算法,leetcode,java,数据结构)