zoukankan      html  css  js  c++  java
  • 漫画:寻找股票买入卖出的最佳时机(动态规划)

    此文转载自:https://my.oschina.net/u/4543837/blog/4700039

    前一段时间,我们介绍了一个经典算法题目:寻找股票买入卖出的最佳时机。这个题目看似简单,却有着许多种变化。


    在上一篇中,我们讲解了最多1次买卖和无限次买卖的解法,那么,如果只允许最多2次股票买卖,如何寻找最佳时机呢?





    我们仍然以之前的数组为例:



    首先,寻找到1次买卖限制下的最佳买入卖出点:


    两次买卖的位置是不可能交叉的,所以我们找到第1次买卖位置后,把这一对买入卖出点以及它们中间的元素全部剔除掉:



    接下来,我们按照同样的思路,再从剩下的元素中寻找第2次买卖的最佳买入卖出点:


    这样一来,我们就找到了最多2次买卖情况下的最佳选择:





    对于上图的这个数组,如果独立两次求解,得到的最佳买入卖出点分别是【1,9】和【6,7】,最大收益是 (9-1)+(7-6)=9:


    但实际上,如果选择【1,8】和【3,9】,最大收益是(8-1)+(9-3)=13>9:



    所谓动态规划,就是把复杂的问题简化成规模较小的子问题,再从简单的子问题自底向上一步一步递推,最终得到复杂问题的最优解。


    首先,让我们分析一下当前这个股票买卖问题,这个问题要求解的是一定天数范围内、一定交易次数限制下的最大收益。


    既然限制了股票最多买卖2次,那么股票的交易可以划分为5个阶段:


    没有买卖

    第1次买入

    第1次卖出

    第2次买入

    第2次卖出


    我们把股票的交易阶段设为变量k(用从0到4的数值表示),把天数范围设为变量n。而我们求解的最大收益,受这两个变量影响,用函数表示如下:


    最大收益 = F(n,k)(0<=k<=4,n>=1)


    既然函数和变量已经确定,接下来我们就要确定动态规划的两大要素:


    1.问题的初始状态
    2.问题的状态转移方程式


    问题的初始状态是什么呢?我们假定交易天数的范围只限于第1天,也就是n=1的情况:



    1.如果没有买卖,也就是k=0时,最大收益显然是0,也就是 F(1,0)= 0


    2.如果有1次买入,也就是k=1时,相当于凭空减去了第1天的股价,最大收益是负的当天股价,也就是 F(1,1)= -price[0]


    3.如果有1次买出,也就是k=2时,买卖抵消(当然,这没有实际意义),最大收益是0,也就是 F(1,2)= 0


    4.如果有2次买入,也就是k=3时,同k=1的情况,F(1,3)= 0 


    5.如果有2次卖出,也就是k=4时,同k=2的情况,F(1,4)= 0



    确定了初始状态,我们再来看一看状态转移。假如天数范围限制从n-1天增加到n天,那么最大收益会有怎样的变化呢?


    这取决于现在处于什么阶段(是第几次买入卖出),以及对第n天股价的操作(买入、卖出或观望)。让我们对各个阶段情况进行分析:


    1.假如之前没有任何买卖,而第n天仍然观望,那么最大收益仍然是0,即 F(n,0) = 0


    2.假如之前没有任何买卖,而第n天进行了买入,那么最大收益是负的当天股价,即 F(n,1)= -price[n-1]


    3.假如之前有1次买入,而第n天选择观望,那么最大收益和之前一样,即 F(n,1= F(n-1,1)


    4.假如之前有1次买入,而第n天进行了卖出,那么最大收益是第1次买入的负收益加上当天股价,即那么 F(n,2= F(n-1,1)+ price[n-1]


    5.假如之前有1次卖出而第n选择观望,那么最大收益和之前一样,即 F(n,2= F(n-1,2)


    6.假如之前有1次卖出,而第n天进行2次买入,那么最大收益是第1次卖出收益减去当天股价,即F(n,3)= F(n-1,2) - price[n-1]


    7.假如之前有2次买入,而n天选择观望那么最大收益和之前一样,即 F(n,3= F(n-1,3)


    8.假如之前有2次买入,而第n天进行了卖出,那么最大收益是第2次买入收益减去当天股价,即F(n,4)= F(n-1,3) + price[n-1]


    9.假如之前有2次卖出,而第n天选择观望(也只能观望了),那么最大收益和之前一样,即 F(n,4= F(n-1,4)


    最后,我们把情况【2,3】,【4,5】,【6、7】,【8,9】合并,可以总结成下面的5个方程式:


    F(n,0) = 0

    F(n,1)=  max(-price[n-1],F(n-1,1)

    F(n,2)=  max(F(n-1,1)+ price[n-1]F(n-1,2)

    F(n,3)=  max(F(n-1,2)- price[n-1]F(n-1,3)

    F(n,4)=  max(F(n-1,3)+ price[n-1]F(n-1,4)


    从后面4个方程式中,可以总结出每一个阶段最大收益和上一个阶段的关系:


    F(n,k) = max(F(n-1,k-1)+ price[n-1],F(n-1,k))


    由此我们可以得出,完整的状态转移方程式如下:



    在表格中,不同的行代表不同天数限制下的最大收益,不同的列代表不同买卖阶段的最大收益。


    我们仍然利用之前例子当中的数组,以此为基础来填充表格:



    首先,我们为表格填充初始状态:



    接下来,我们开始填充第2行数据。


    没有买卖时,最大收益一定为0,因此F(2,0的结果是0:



    根据之前的状态转移方程式,F(2,1)= max(F(1,0)-2,F(1,1))= max(-2,-1)= -1,所以第2行第2列的结果是-1:



    F(2,2)= max(F(1,1)+2,F(1,2))= max(1,0)= 1,所以第2行第3列的结果是1:



    F(2,3)= max(F(1,2)-2,F(1,3))= max(-2,-1)= -1,所以第2行第4列的结果是-1:


    F(2,4)= max(F(1,3)+2,F(1,4))= max(1,0= 1,所第2行第5列的结果是1



    接下来我们继续根据状态转移方程式,填充第3行的数据:



    接下来填充第4行:



    以此类推,我们一直填充完整个表格:



    如图所示,表格中最后一个数据13,就是全局的最大收益。


        //最大买卖次数
        private static int MAX_DEAL_TIMES = 2;

        public static int maxProfitFor2Time(int[] prices) {
            if(prices==null || prices.length==0) {
                return 0;
            }
            //表格的最大行数
            int n = prices.length;
            //表格的最大列数
            int m = MAX_DEAL_TIMES*2+1;
            //使用二维数组记录数据
            int[][] resultTable = new int[n][m];
            //填充初始状态
            resultTable[0][1] = -prices[0];
            resultTable[0][3] = -prices[0];
            //自底向上,填充数据
            for(int i=1;i<n;++i) {
                for(int j=1;j<m;j++){
                    if((j&1) == 1){
                        resultTable[i][j] = Math.max(resultTable[i-1][j], resultTable[i-1][j-1]-prices[i]);
                    }else {
                        resultTable[i][j] = Math.max(resultTable[i-1][j], resultTable[i-1][j-1]+prices[i]);
                    }
                }
            }
            //返回最终结果
            return resultTable[n-1][m-1];
        }




        //最大买卖次数
        private static int MAX_DEAL_TIMES = 2;

        public static int maxProfitFor2TimeV2(int[] prices) {
            if(prices==null || prices.length==0) {
                return 0;
            }
            //表格的最大行数
            int n = prices.length;
            //表格的最大列数
            int m = MAX_DEAL_TIMES*2+1;
            //使用一维数组记录数据
            int[] resultTable = new int[m];
            //填充初始状态
            resultTable[1] = -prices[0];
            resultTable[3] = -prices[0];
            //自底向上,填充数据
            for(int i=1;i<n;++i) {
                for(int j=1;j<m;j++){
                    if((j&1) == 1){
                        resultTable[j] = Math.max(resultTable[j], resultTable[j-1]-prices[i]);
                    }else {
                        resultTable[j] = Math.max(resultTable[j], resultTable[j-1]+prices[i]);
                    }
                }
            }
            //返回最终结果
            return resultTable[m-1];
        }


    在这段代码中,resultTable从二维数组简化成了一维数组。由于最大买卖次数是常量,所以算法的时间复杂度也从O(n)降低到了O(1)。



        public static int maxProfitForKTime(int[] prices, int k) {
            if(prices==null || prices.length==0) {
                return 0;
            }
            //表格的最大行数
            int n = prices.length;
            //表格的最大列数
            int m = k*2+1;
            //使用一维数组记录数据
            int[] resultTable = new int[m];
            //填充初始状态
            resultTable[1] = -prices[0];
            resultTable[3] = -prices[0];
            //自底向上,填充数据
            for(int i=1;i<n;++i) {
                for(int j=1;j<m;j++){
                    if((j&1) == 1){
                        resultTable[j] = Math.max(resultTable[j], resultTable[j-1]-prices[i]);
                    }else {
                        resultTable[j] = Math.max(resultTable[j], resultTable[j-1]+prices[i]);
                    }
                }
            }
            //返回最终结果
            return resultTable[m-1];
        }




    —————END—————



    喜欢本文的朋友,欢迎关注公众号 程序员小灰,收看更多精彩内容

    
          
          
    点个[在看],是对小灰最大的支持!


    本文分享自微信公众号 - 程序员小灰(chengxuyuanxiaohui)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

       

    更多内容详见微信公众号:Python测试和开发

    Python测试和开发

  • 相关阅读:
    Tomcat原理与实践
    Spring Boot入门与实践
    Docker安装及使用
    JDK源码解析——集合(一)数组 ArrayList
    浅谈mysql底层索引
    微信小程序全局配置知识点
    uniapp全屏高度
    npm node-sass报错
    微信小程序接口配置问题
    微信小程序的设计流程
  • 原文地址:https://www.cnblogs.com/phyger/p/14048175.html
Copyright © 2011-2022 走看看