股票风云

leetcode热衷于通过探讨股票的买入和卖出时机,来教我们炒股,一共有6个相关题目(算上剑指offer一共7个)以及一个股票价格跨度计算器,几乎全部都可以用dp解决。可见理财本质就是规划。

一锤子买卖

121. 买卖股票的最佳时机 是股票买卖时机的入门题目,与剑指 Offer 63. 股票的最大利润 是同一题。可以利用 53题线段和 的思想来做,也就是先差分,再求最大和:

              public int maxProfit(int[] prices) {
                      int len = prices.length;
                      if(len<2)return 0;
              
                      int max = 0,temp=0;
                      for(int i=1;i<len;++i){
                          if(temp<0)temp = (prices[i] - prices[i-1]);
                          else temp+=(prices[i] - prices[i-1]);
                          max = Math.max(temp,max);
                      }
                      return max;
                  }

这个题目很简单,也透露着朴素的道理:如果只能做一次选择,那就不需要纠结。

散户的理想

122. 买卖股票的最佳时机 II 是股票买卖时机的第二个题目。这个题目比第一个还简单,有dp和greedy两种方法。它大概描述了一个散户的终极理想:永远可以在低点买入高点卖出。没有任何手续费和次数限制,哪怕高点和低点只差一毛钱,也可以进行一次买卖:

              public int maxProfit(int[] prices) {
                  int len = prices.length;
                      if(len<2)return 0;
              
                      int max = 0,temp=0;
                      for(int i=1;i<len;++i){
                          temp = prices[i] - prices[i-1];
                          if(temp>0) max+=temp;
                      }
                      return max;
          
              }

现实中我们无法没有这种理想的情景,所以这个题目反向说明一个道理:散户七赔二平一赚,主要原因在于短时。频繁的交易往往是带来巨额赔损以及心态崩溃。无怪乎赚钱的往往是忘记交易密码的玩家。

最佳散户

123. 买卖股票的最佳时机 III 是股票买卖时机的第三个题目,难度陡增。它大概描述了一个佛系且明智的散户的状态:在合适的价格买入,然后长期持有,在合适的价格卖出,然后等待下次合适的买入时机。
这个题目,容易无从下手。实际有2种dp方法。
方法一,相对容易理解和想到。从中间分割为2笔交易,左边的找到最小值,当前值减去最小值即当前点的最大收益;右边从右向左运行,找到最大值,减去当前值即为右边的最大值;然后左边两边遍历结果,求每个的和,比较每个和的最大值:

      public int maxProfit(int[] prices){
        int len = prices.length;
        if (len<2)
            return 0;


        int[] dp_left = new int[len],dp_right = new int[len];

        int min=prices[0];
        for (int i = 1; i <len; i++) {
            dp_left[i] = Math.max(dp_left[i-1],prices[i] - min );
            min = Math.min(min,prices[i]);
        }

        int  max = prices[len-1];
        for (int i = len-2; i >0; i--) {
            dp_right[i] = Math.max(dp_right[i+1],max - prices[i]);
            max = Math.max(max,prices[i]);
        }

        max = 0;
        for (int i = 1; i < len; i++) {
            max = Math.max(max,dp_left[i] + dp_right[i]);
        }

        return max;
        
    }

方法二,较难想出,更难理解,也更难实现;但更有通用性(能适用于分割成2、3.。。。k笔交易),也是更高阶的状态dp。思路如下:
第一笔的买入卖出是正常操作,第二笔的买入基于第一笔卖出的最大收益,第二笔的卖出在第二笔买入的基础上继续计算,整个是个累加递进的状态的过程。
四种状态可以简化为四个int变量,为了更好的体现思路,用二维数组替代
      public int maxProfit(int[] prices){
                int len = prices.length;
        if (len<2)
            return 0;

        int[][] dp = new int[len][4];
        dp[0][0] = -prices[0];
        dp[0][2] = -prices[0];


        //记录前缀状态
        for (int i = 1; i < len; i++) {
            dp[i][0] = Math.max(-prices[i],dp[i-1][0]);//持有第一笔股票的最好值
            dp[i][1] = Math.max(dp[i-1][1],prices[i] + dp[i-1][0]);//卖出第一笔股票的最好值
            dp[i][2] = Math.max(dp[i-1][2],dp[i-1][1]-prices[i]);//持有第二笔股票的最好值
            dp[i][3] = Math.max(dp[i-1][3],prices[i] + dp[i-1][2]);//卖出第二笔股票的最好值


            }


        return dp[len-1][3];//返回第二笔收益,因为第二笔会继承第一笔的收益
        
    }

庄家行为

188. 买卖股票的最佳时机 IV 是股票买卖时机的第四个题目,算是第三个题目的渐进版本。它描述了一个佛系典型的操盘手的状态:在合适的价格买入,然后长期持有,在合适的价格卖出,然后等待下次合适的买入时机。与[最佳散户]不同的是,操盘手可以有多次屯货和高位卖出的机会
从dp的角度来说,这个题目即第三个题目的第二种方法的通用版本。代码如下:

       public int maxProfit(int k, int[] prices) {
        int len = prices.length;
        if (len < 2)
            return 0;

        int[][][] dp = new int[len][k][2];
        for (int i = 0; i < k; i++) {
            dp[0][i][0] = -prices[0];
        }

        //记录前缀状态
        for (int i = 1; i < len; i++) {

            for (int j = 0; j < k; j++) {
                if (j > 0)
                    dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j - 1][1] - prices[i]);//持有第j笔股票的最好值=j-1笔收益-购买当前股票
                else dp[i][j][0] = Math.max(dp[i - 1][j][0], -prices[i]);//持有第j笔股票的最好值=j-1笔收益-购买当前股票

                dp[i][j][1] = Math.max(dp[i - 1][j][1], prices[i] + dp[i - 1][j][0]);//卖出第j笔股票的最好值
            }

        }

        return dp[len - 1][k - 1][1];
    }

和第三个题目相同,三维数组可以优化到二维,为了更清晰的表示含义,保留了空间占用更大的三维。

回到现实

714. 买卖股票的最佳时机含手续费 是股票买卖时机的第五个题目,也算是番外篇1。它描述了一个现实中的股票交易场景:每次卖出,是要收手续费的。这也是股票乃至于基金交易的必须考虑的问题:频繁的交易,往往是给证券公司或者基金公司打工。
从dp的角度来说,每一天有2个状态:第一种是买入,两种种情况:1,不操作,从上一步的买入的收益延续下来;2,买入,从上步的卖出的收益延续。第二种是卖出,两种情况比较:1,不操作,还是从上一步卖出的收益延续下来;2,卖出,从上一步的买入延续下来。代码如下:

       public int maxProfit(int k, int[] prices) {
        int len = prices.length;
        if (len < 2)
            return 0;

        int[][] dp = new int[len][2];//0为买入,1为卖出
        dp[0][0] = - prices[0];//持有股票的情况,第一天只有买入这一种情况,无法从之前买入的持有下去
        dp[1][0] = Math.max(dp[0][0],-prices[1]);//持有股票的情况
        dp[1][1] = Math.max(prices[1]-prices[0]- fee,0);
        for (int i = 2; i < len; i++) {
            //卖出,两种情况比较:1,不操作,还是从上一步卖出的收益延续下来;2,卖出,从上一步的买入延续下来 ++
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] + prices[i]-fee);
            //买入,两种种情况:1,不操作,从上一步的买入的收益延续下来;2,买入,从上步的卖出的收益延续 --
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
        }

        return Math.max(dp[len-1][1],0);
    }

这个题目暗示了散户理财的归宿:给理财公司当韭菜。

股东行为

309. 最佳买卖股票时机含冷冻期 是股票买卖时机的第六个题目,也算是番外篇2。它描述了一个现实中的股东股票交易场景:每次卖出,是含冷冻期的。。
从dp的角度来说,每一天有2个状态:第一种是买入,两种种情况:1,不操作,从上一步的买入的收益延续下来;2,买入,从上步的卖出的收益延续。第二种是卖出,两种情况比较:1,不操作,还是从上一步卖出的收益延续下来;2,卖出,从上上步的买入延续下来。代码如下:

       public int maxProfit(int k, int[] prices) {
        int len = prices.length;
        if (len < 2)
            return 0;

        int[][] dp = new int[len][2];//0为买入,1为卖出
        dp[0][0] = - prices[0];//持有股票的情况,第一天只有买入这一种情况,无法从之前买入的持有下去
        dp[1][0] = Math.max(dp[0][0],-prices[1]);//持有股票的情况
        dp[1][1] = Math.max(prices[1]-prices[0],0);
        for (int i = 2; i < len; i++) {
            //卖出,两种情况比较:1,不操作,还是从上一步卖出的收益延续下来;2,卖出,从上一步的买入延续下来 ++
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] + prices[i]);
            //买入,两种种情况:1,不操作,从上一步的买入的收益延续下来;2,买入,从上上步的卖出种延续 --
            dp[i][0] = Math.max(dp[i-1][0],dp[i-2][1]-prices[i]);
        }

        return Math.max(dp[len-1][1],0);
    }

股票跨度计算器

901. 股票价格跨度 是股票买卖时机的第七个题目,也算是番外篇3。它收集某些股票的每日报价,并返回该股票当日价格的跨度。可以理解为股民们喜欢的K线的

List<int[]> list;

    public StockSpanner() {
        list = new ArrayList<>();

    }
    
    public int next(int price) {

        if(list.size()==0) {
            list.add(new int[]{price,1});
            return 1;
        }else{
            int index = list.size()-1,score=1;
            while(index>-1 && list.get(index)[0]<=price){
                score+= list.get(index)[1];
                index-= list.get(index)[1];
            }
            list.add(new int[]{price,score});
            return score;
        }

    }

至此,leetcode已经教完我们炒股,是时候到股市里为城市绿化做贡献了。

相关资料: 股票问题系列通解


Is this article useful to you? How about buy me a coffee ?