首页 > 技术文章 > 算法系列-动态规划(4):买卖股票的最佳时机

lillcol 2020-12-31 23:01 原文

此系列为动态规划相关文章。

系列历史文章:
算法系列-动态规划(1):初识动态规划

算法系列-动态规划(2):切割钢材问题

算法系列-动态规划(3):找零钱、走方格问题

算法系列-动态规划(4):买卖股票的最佳时机


新生韭菜罗拉

自从上次看到八哥收藏旧币,罗拉也想给自己捣鼓个副业,赚点零花钱。

于是她瞄上了股票,作为股场新人,罗拉可是满怀信心的。
觉得自己只要顺应潮流,识大体,懂进退,不贪心,即使不赚大钱,也不至于亏钱。 所以她想拿个一千八百试试水。

八哥作为过来人,股票玩得稀碎,当年也是这么过来的,狠狠的当了一波韭菜。
但是看罗拉的劲头,不被收割一次是劝不住她的富婆梦的。
就看看她怎么捣鼓吧。

罗拉这几天一直盯着手机看股票行情。
时而欣喜,时而叹气。

看来时机差不多了,八哥准备落井...关心一下罗拉。

对话记录

**八哥**

罗拉,炒股也有几天了,你的富婆梦是否近了一步?|

**罗拉**

哎,别提了,这几天天天盯着价格,眼睛都花了。
我买股票好像就跟我做对一样,在我手上狂跌,我一卖就涨

| | |

**八哥**

是不是有一种,这些股票专门割你韭菜的赶脚。
只要持有就跌,卖出后就涨。
全世界都盯着你的一千八百|

**罗拉**

对啊,这几天我只关注一支股票,也一直在买卖这个。
虽然不至于像你说的这么夸张,但是确实现在小亏吧。
要么因为下跌急急忙忙卖了,但是我一卖它马上又涨回来了
要么因为上涨态势好,我持有了,但是转眼它又跌了
总之,时机把握的不好

| | |

**八哥**

这么看来你的富婆梦不怎么顺利呀|

**罗拉**

确实,果然小丑是我自己吗?
也对,要是这么容易,谁还老老实实干活啊,都去炒股的了| | |

**八哥**

是的
所以我一开始也不劝你,毕竟大家开始的心态和你都差不多。
只有被割过韭菜,才会知道炒股高风险,高回报。不是一般人能玩的。|

**罗拉**

哎,白白浪费了几天时间,
看来我还是适合去玩鸡,找个靠谱鸡放着都比这个强| | |

**八哥**

富婆梦可能毫无收获,但是这个经历到时可以用来提升一下自己,
买卖股票可是一个很经典的算法题哦。
当然这个事后诸葛亮的题目。|

**罗拉**

算法?有点意思,说来瞅瞅| | |

**八哥**

行,我把几个经典的案例说一下吧|

说到炒股,
想当年八哥的神操作...,泪流满面

八哥韭菜曲线


买卖股票的最佳时机(交易一次)

“先来第一个题目,”
“罗拉,你把你最近的股票七八天的股价给我列一下”

“好,最近七八天的价格是这样的:{9,11,7,5,7,10,18,3}

“嗯,我们现在先来最简单的,题目如下:”

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),
设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。

“你试着分析看看。”

“行我试试”

“要想一次交易收益最大”,
“那么必须保证我是在最低点买入最高点卖出。这个样就可以保证我的收益最大”。
“在这里我们的最低点是3,最高点是18,这样算的话最大收益是15”。

“嗯,不对,3是在18后面,这样不符合逻辑”。
“应该是要保证最低价格在最高价格前面,要先买了才能买”。

“所以,假设今天是第i天,我只要记录i-1天之前的最低价格”,
“用今天的价格减去最低价格得到利润,然后选取最大的利润即可”。
“嗯,典型动态规划特征”。
“我用dp[i]记录前i-1天的最低价格,”
“边界值为第0天股价设置为最大,保证dp[1]以后最小值”。
“哈哈,姐姐明白了”。

罗拉自信道,然后开始编码。

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {9, 11, 7, 5, 7, 10, 18, 3};
        int[] pricesUp = {2,3,4,5,6,7,8,9};
        int[] pricesDown = {9,8,7,6,5,4,3,2};
        System.out.println("prices 一次交易最大利润为: "+stockTrading1(prices));
        System.out.println("pricesUp 一次交易最大利润为: "+stockTrading1(pricesUp));
        System.out.println("pricesDown 一次交易最大利润为: "+stockTrading1(pricesDown));
    }

    public static int stockTrading1(int[] prices) {
        if(prices==null || prices.length<2) return 0;

        int[] dp = new int[prices.length + 1];
        //设定边界
        dp[0] = Integer.MAX_VALUE;//为了后面能取到最小值,dp[0]设置为Integer.MAX_VALUE
        int max = Integer.MIN_VALUE;//一开始利润为Integer.MIN_VALUE
        for (int i = 1; i <= prices.length; i++) {
            max = Math.max(max,prices[i-1] - dp[i-1]);
            dp[i] = Math.min(dp[i - 1], prices[i-1]);
        }
        return max>=0?max:0;//利润不能为负数,毕竟没有傻子
    }
}
//输出结果
prices 一次交易最大利润为: 13
pricesUp 一次交易最大利润为: 7
pricesDown 一次交易最大利润为: 0

“不错,结果也没错,可是你没必要万事皆动态吧”。
“吹毛求疵,如果我要用O(1)的时间复杂度,咋整?” 八哥有气无力道。
“明明有简单点、更高效的写法,比如这样: ”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {9, 11, 7, 5, 7, 10, 18, 3};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 一次交易最大利润为: " + stockTrading1(prices));
        System.out.println("pricesUp 一次交易最大利润为: " + stockTrading1(pricesUp));
        System.out.println("pricesDown 一次交易最大利润为: " + stockTrading1(pricesDown));
    }

    public static int stockTrading1(int[] prices) {
        if (prices == null || prices.length < 2) return 0;

        //设定边界
        int min = Math.min(prices[0], prices[1]);//记录前两天的最低价格
        int max = prices[1] - prices[0];//记录前两天利润
        for (int i = 2; i < prices.length; i++) {
            max = Math.max(max, prices[i] - min);
            min = Math.min(min, prices[i]);
        }
        return max >= 0 ? max : 0; //利润不能为负数,毕竟没有傻子
    }
}
//输出结果
prices 一次交易最大利润为: 13
pricesUp 一次交易最大利润为: 7
pricesDown 一次交易最大利润为: 0

“不过这个一般问题不大,我只是想说你不要陷入一个误区就是啥都钻到动态里面去”。
“而忽视其他的方法”。

“哦,自从学了动态,好像确实有点凡事只想动态了,不过你这个本质还是动态吧”,罗拉尴尬道。

“嗯,这么说也没错,就是压缩一下空间而已,能想到动态不是坏事,只要不钻牛角尖就好了”。
“这是最简单的,接下来我们看看下一个问题”。


买卖股票的最佳时机(交易多次)

“第二个问题如下:”

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。
你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

“这是股票问题的第二类,你看看这个要怎么处理”。

“嗯,我先看看”,
“如果我要算最大的,利润,肯定是得逢低买入,逢高卖出”。
“不过有个限制条件,每天只能进行一次交易,只能买卖二选一”。
“我可以比较两天的价格,如果第i天的价格prices[i]大于第i-1天的价格prices[i-1],那么我就在第i-1天买入,第i天卖出”。
“但是这会存在一个问题,如果我连续两天都是上涨的,这样算会出问题”。
“比如prices[i-2]<prices[i-1]<prices[i],此时我按照上面的做法,prices[i-2]买入,prices[i-1]是卖出的,那prices[i]这一块最多只能是买入了,显然不合逻辑”。
“那我修正一下逻辑”。
“我找每一个上升区间[p1,p2],在p1买入,在p2卖出即可,就像这张图里面绿色部分”。

递增区间

“然后只要把每一段的利润加起来就可以了,so easy” 罗拉得意道。

“不错,思路可以,show me your code”。八哥点点头。

“行,稍后”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {7, 2, 5, 3, 6, 4, 7, 8, 2};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading2(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading2(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading2(pricesDown));
    }

    public static int stockTrading2(int[] prices) {
        if (prices == null || prices.length < 2) return 0;
        int profit = 0;
        for (int i = 1; i < prices.length; i++) {
            //转换成求解上升区间的问题
            if (prices[i] > prices[i - 1]) profit += (prices[i] - prices[i - 1]);
        }
        return profit;
    }
}
//输出结果
prices 不限次交易最大利润为: 10
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

“不错,很简单了,不过既然这是动态规划的经典案例,你再试试动态呗”。八哥笑道

“有必要吗?这不都做出来了吗,而且这个时间复杂度O(n)已经比动态好了吧”。罗拉不解

“话是这么说没错,不过这个虽然动态不是最优解,但是这个思路可以借鉴。这是思想”。

“行吧,我试试”。

“我第i天的收益受到第i-1天利润的影响”
“但是每天其实就只有两个状态,是否持有股票”

“我可以用一个二维数组dp[prices.length+1][2]的数组来记录每天不同状态的最大利润”

“其中dp[i][0]表示第i天不持有股票”。
“会存在两种情况:”
“1. 前一天不持有,即:dp[i-1][0]
“2. 前一天持有,今天卖出,即:dp[i-1][1]+prices[i]

“其中dp[i][1]表示第i天持有股票”。
“也会存在两种情况:”
“1. 前一天持有,今天不卖出继续持有,即:dp[i-1][1]
“2. 前一天不持有,今天买入,即:dp[i-1][0]-prices[i]

“对于边界值:”
“第一天的两种情况为:”
dp[1][0] = 0
dp[1][1] = -prices[0]

“所以代码实现为:”


public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {7, 2, 5, 3, 6, 4, 7, 8, 2};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading2(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading2(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading2(pricesDown));
    }

    public static int stockTrading2(int[] prices) {
        if (prices == null || prices.length < 2) return 0;
        int[][] dp = new int[prices.length + 1][2];//此处prices.length + 1是为了计算方便,所以后面的prices[i - 1]是相应调整的
        //初始化边界值
        //第一天不持有,利润为0,持有的即买入,此时利润为-prices[0]        
        dp[1][0] = 0;
        dp[1][1] = -prices[0];
        for (int i = 2; i < dp.length; i++) {
            //今天不持有有股票的情况为:
            //1. 前一天不持有,即:dp[i-1][0]
            //2. 前一天持有,今天卖出,即:dp[i-1][1]+prices[i-1]
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            //今天持有股票的情况为
            //1. 前一天持有,今天继续持有,即:dp[i-1][1]
            //2. 前一天不持有,今天买入,即:dp[i-1][0]-prices[i-1]
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
        }
        //最后一天不持有应该是收益最大的,所以没必要再比较dp[prices.length][0],dp[prices.length][1]了
        return dp[prices.length][0];
    }

}
//输出结果
prices 不限次交易最大利润为: 10
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

“嗯,不错,那有没有可以优化的方法呢?比如空间复杂度我要O(1)。”八哥继续追问

“嗯,我想想。”
“有了,因为他其实只和前一天的状态有关那么我只需要记录前一天的两个状态就可以了。”
“可以这样实现。”罗拉兴奋道

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {7, 2, 5, 3, 6, 4, 7, 8, 2};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading2(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading2(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading2(pricesDown));
    }

    public static int stockTrading2(int[] prices) {
        if (prices == null || prices.length < 2) return 0;
        //sell表示当天不持有股票,buy表示当天持有股票,其中第一天的状态如下
        int sell = 0,buy = -prices[0],tmp=0;
        for (int i = 1; i < prices.length; i++) {
            //今天不持有有股票的情况为:
            //1. 前一天不持有,即:sell
            //2. 前一天持有,今天卖出,即:buy+prices[i]
            tmp = sell;
            sell = Math.max(sell, buy + prices[i]);
            //今天持有股票的情况为
            //1. 前一天持有,今天继续持有,即:buy
            //2. 前一天不持有,今天买入,即:tmp-prices[i]
            buy = Math.max(buy, tmp - prices[i]);
        }
        //最后一天不持有应该是收益最大的,所以没必要再比较sell,buy
        return sell;
    }

}
//输出结果
prices 不限次交易最大利润为: 10
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

“我可以通过两个标签记录该状态,达到降低空间复杂度的目的。”罗拉很得意自己想到办法了。

“是的,其实很多动态都可以通过类似方式优化,本质上还是动态规划。”
“看来第二种类型你也掌握的差不多了,是时候看看第三种了。”

“还有?快说。”罗拉可是自信满满的

“还有好几种呢,别急”


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

“第三种的题目如下:”

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

“其中一笔交易过程为:”

交易过程

“这个不难吧,只要在前一个案例中出售股票的位置减掉手续费即可”
“代码如下:”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {1, 3, 2, 8, 4, 9};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading3(prices,2));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading3(pricesUp,2));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading3(pricesDown,2));
    }

    public static int stockTrading3(int[] prices,int fee) {
        if (prices == null || prices.length < 2) return 0;
        //sell表示当天不持有股票,buy表示当天持有股票,其中第一天的状态如下
        int sell = 0,buy = -prices[0],tmp=0;
        for (int i = 1; i < prices.length; i++) {
            //今天不持有有股票的情况为:
            //1. 前一天不持有,即:sell
            //2. 前一天持有,今天卖出,此时需要支付手续费,即:buy+prices[i]-fee
            tmp = sell;
            sell = Math.max(sell, buy + prices[i]-fee);
            //今天持有股票的情况为
            //1. 前一天持有,今天继续持有,即:buy
            //2. 前一天不持有,今天买入,即:tmp-prices[i]
            buy = Math.max(buy, tmp - prices[i]);
        }
        //最后一天不持有应该是收益最大的,所以没必要再比较sell,buy
        return sell;
    }
}
//输出结果
prices 不限次交易最大利润为: 8
pricesUp 不限次交易最大利润为: 5
pricesDown 不限次交易最大利润为: 0

“其他版本大同小异,我就不写了,赶紧来点有难度的。”罗拉不屑道

“哎,年轻人,别毛毛躁躁,请看下一题”


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

“下面是第四种类型,题目如下:”

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票)。
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

“完整交易周期为:”

“你看看此时要怎么做?”

“我看看,还想比之前复杂,不过应该也是一脉相传,”罗拉自语

“首先现在是有三个状态,卖出,买入、冷冻期。”
“那我可以定义三个状态0,1,2分别表示三个状态。”
“定义动态数据dp[i][j],表示第i天,状态j的最大利润,其中j的取值为{0,1,2}

“那么此时的状态转移可以总结如下:”

状态 含义 转换 注释
dp[i][0] 此时为卖出状态 max(dp[i - 1][0], dp[i - 1][1] + prices[i]) 此时存在两种情况
1. 前一天是已经卖出了
2. 前一天处于买入状态今天卖出
dp[i][1] 此时为买入状态 max(dp[i - 1][1], dp[i - 1][2] - prices[i]) 此时存在两种情况
1. 前一天为买入状态
2. 前一天为冷冻期,今天买入
dp[i][2] 此时为冷冻期 dp[i - 1][0] 此时存在一种情况
1. 前一天为卖出状态

“此时代码实现如下”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {1, 3, 2, 8, 4, 9};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading4(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading4(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading4(pricesDown));
    }

    public static int stockTrading4(int[] prices) {
        if (prices == null || prices.length < 2) return 0;
        int n = prices.length;
        int[][] dp = new int[n][3];
        //初始化边界(卖出,买入)
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[1][2] = 0;
        for (int i = 1; i < n; i++) {
            //此时为卖出状态,要么前一天是已经卖出了dp[i-1][0],要么就是昨天处于买入状态今天卖出获得收益dp[i-1][1]+prices[i]
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            //此时为买入状态,只能是前一天为买入状态dp[i-1][1]或者前一天为冷冻期,今天买入,花费金钱dp[i-1][2]-prices[i]
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2] - prices[i]);
            //此时为冷冻期,此时只有前一天为卖出状态``dp[i-1][0]``,今天不操作
            dp[i][2] = dp[i - 1][0];
        }
        return Math.max(dp[n-1][0], dp[n - 1][2]);
    }
}
//输出结果
prices 不限次交易最大利润为: 8
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

“不错,这个动态比较好理解,那你接下来可以在这基础上做一下空间压缩吗?”八哥继续追问。

“应该问题不大,我试试。”
“根据上边的推到公式,我们可以知道动态状态转移为:”

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][2] - prices[i]);
dp[i][2] = dp[i - 1][0];

“而最终的结果为:”

Math.max(dp[n - 1][0], dp[n - 1][2])

“根据这两个dp[n - 1][0], dp[n - 1][2]可知:”
“我们计算当日的最大利润只跟dp[i - 1][0],dp[i - 1][1],dp[i - 1][2]有关”
“即之和前一天的卖出、买入、冷冻期的最大利润有关”
“所以我们只需要记录最新的三个状态即可,无需记录所有的状态。”
“实现如下:”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {1, 3, 2, 8, 4, 9};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading4(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading4(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading4(pricesDown));
    }

    public static int stockTrading4(int[] prices) {
        if (prices == null || prices.length < 2) return 0;
        int n = prices.length;
        //初始化边界(卖出,买入)
        int dp0 = 0,dp1 = -prices[0],dp2= 0,tmp;
        for (int i = 1; i < n; i++) {
            tmp=dp0;
            //此时为卖出状态,要么前一天是已经卖出了dp0,要么就是昨天处于买入状态今天卖出获得收益dp1+prices[i]
            dp0 = Math.max(dp0, dp1 + prices[i]);
            //此时为买入状态,只能是前一天为买入状态dp1或者前一天为冷冻期,今天买入,花费金钱dp2 -prices[i]
            dp1 = Math.max(dp1, dp2 - prices[i]);
            //此时为冷冻期,此时只有前一天为卖出状态``dp0``,今天不操作
            dp2 = tmp;
        }
        return Math.max(dp0, dp2);
    }
}
//输出结果
prices 不限次交易最大利润为: 8
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

“这样的话空间复杂度就可以下降到O(1)和之前的方法类似”

“是的,压缩空间是动态规划常用的优化方法,一般只要是依赖的状态只是前一个或几个状态,我们就可以通过类似的方法优化。”

“第四种你也做出来了,要不要来第五个?”八哥笑道

“还有?还有几个?”罗拉显然有点吃惊

“还有两个,前面的相对简单的,后面这两个有点难度,要试试不?”

“试试吧,都这个时候,放弃有点不甘心。” 罗拉一咬牙有了决断

“有志气,请听题”


买卖股票的最佳时机(最多交易两次)

“题目是:”

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

完整的两次交易为:

完整的两次交易

“你看看这个要怎么分析?有点难度哦”

“嗯,我试试”
“这个其实类似第二个场景吧,就是:买卖股票的最佳时机(交易多次)”
“这不过这里限制了两次,当时只考虑当天的状态为:持有或者卖出,此时控制考虑了一个维度”
“但是我们除了考虑当天是持有,还是卖出外,还得考虑是第几次交易,我们最多只能进行两次交易,也就是我们还缺一个状态”

“既然这样我们可以增加一个状态,记录已经交易了几次,即已经卖出多少次”
“这样我们的状态数据就可以变成dp[天数][当前是否持股][卖出的次数]=>dp[i][j][k]
“这样每天会有六个状态,分别为:”

编号 状态 含义 状态转换 备注
1 dp[i][0][0] i天不持有股票
交易0
0 或 dp[i-1][0][0] 即从头到尾都没有交易过,利润为0
没有进行过交易
2 dp[i][0][1] i天不持有股票
交易1
max(dp[i-1][1][0] + prices[i], dp[i-1][0][1]) 此时可能情况为:
1. 前一天持有,在今天卖出
2. 前一天不持有,很早以前就卖了一次
ps:已经完成了第一轮交易
3 dp[i][0][2] i天不持有股票
交易2
max(dp[i-1][1][1] + prices[i], dp[i-1][0][2]) 此时可能情况为:
1. 前一天持有,今天卖出
2. 前一天不持有,很早以前卖出
ps:已经完成了第二轮交易
4 dp[i][1][0] i天持有股票
交易0
max(dp[i-1][1][0], dp[i-1][0][0] - prices[i]) 此时可能情况为:
1. 前一天就持有股票,今天继续持有
2. 前一天未持今天买入
ps:进行第一轮交易的持有操作
5 dp[i][1][1] i天持有股票
交易1
max(dp[i-1][1][1], dp[i-1][0][1] - prices[i]) 此时可能情况为:
1. 前一天就持有股票,今天继续持有
2. 前一天未持有今天买入
ps:进行第二轮交易的持有操作
6 dp[i][1][2] i天持有股票
交易2
0 此时超出我们交易次数限制
直接返回0即可

“关于最终结果”
“我可以是交易一次,也可以是交易两次,也可以不交易,只要保证利润最大即可”

“至于初始值”
“第一天只有第一次买入和不操作才是正常,其他四种情况都是非法,直接给个最小值就可以了”

“你看我分析的对吗?”

“可以啊,罗拉,你这思路很正确啊”八哥看了罗拉分析,有点惊讶罗拉居然一次就分析出来了。

“毕竟也做了这么多动态了,还有前面几个案例打底,应该的,应该的。”
罗拉也是小小骄傲一下,当然还是得低调。

“既然分析出来了,show me your code”

“行,既然都写出状态转换了,代码还不容易?等着”

几分钟后

“诺,实现了,你看看”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {3, 3, 5, 0, 0, 3, 1, 4};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading5(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading5(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading5(pricesDown));
    }

    public static int stockTrading5(int[] prices) {
        //对于任意一天,我可能得状态为持股,或者不持股,并且可能已经交易了多次了。
        //所以我可以记录第i天的两个维度的状态:dp[天数][当前是否持股][卖出的次数]=>dp[i][j][k]
        //所以每一天就有六种情况,分别为
        //1. 第i天不持有股票,交易0次,即从头到尾都没有交易过,,利润为0,(没有进行过交易)
        //dp[i][0][0] = 0;(也可以写成 dp[i][0][0] = dp[i - 1][0][0],因为前一天也一定是一样的状态)
        //2. 第i天不持有股票,交易1次,此时可能是昨天持有,未卖出过,在今天卖出(今天才卖);或是昨天不持有,但是已经买出一次(很早以前就卖了)(已经完成了一轮轮交易)
        //dp[i][0][1] = Math.max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
        //3. 第i天不持有股票,交易2次,此时可能是此时可能是昨天持有并且已经卖出一次,第二次持有未卖出过,在今天卖出(今天才卖);或是昨天不持有,但是已经买出两次(此时为第一轮买入)(已经完成了两轮交易)
        //dp[i][0][2] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
        //4. 第i天持有股票,交易0次,此时可能是前一天就持有股票,今天继续持有;或者昨天未持今天买入(此时为第一轮买入)
        //dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][0] - prices[i]);
        //5. 第i天持有股票,交易1次,此时可能是前一天就持有股票,今天继续持有;或者昨天未持今天买入(此时为第二轮买入)
        // dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][1] - prices[i]);
        //6. 第i天持有股票,交易2次,此时超出我们交易次数限制,直接返回0即可
        //dp[i][1][2] = 0;
        int[][][] dp = new int[prices.length + 1][2][3];
        //不操作,所以利润为0
        dp[0][0][0] = 0;
        //买入股票,所以为支出
        dp[0][1][0] = -prices[0];
        //不可能情况
        int MIN_VALUE = Integer.MIN_VALUE >> 1;//因为最小值再减去1就是最大值Integer.MIN_VALUE-1=Integer.MAX_VALUE,所以不能直接用最小值,可以极限设置为int MIN_VALUE = - prices[0] - 1;
        dp[0][0][1] = MIN_VALUE;
        dp[0][0][2] = MIN_VALUE;
        dp[0][1][1] = MIN_VALUE;
        dp[0][1][2] = MIN_VALUE;

        for (int i = 1; i < prices.length; i++) {
            dp[i][0][0] = 0;
            dp[i][0][1] = Math.max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
            dp[i][0][2] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
            dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][0] - prices[i]);
            dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][1] - prices[i]);
            dp[i][1][2] = 0;

        }
        //最终的结果我可以是交易一次,也可以是交易两次,也可以不交易,但是不管怎样,最终的状态都是不持有股票
        return Math.max(dp[prices.length - 1][0][1], dp[prices.length - 1][0][2] > 0 ? dp[prices.length - 1][0][2] : 0);
    }
}
//输出结果
prices 不限次交易最大利润为: 6
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

注意:这里MIN_VALUE一定要设置为比(-prices[0])小,具体原因,看看转换关系就知道了。

“不错,现在问题来了,你能压缩一下空间吗?毕竟三维数据是需要占据一定空间的。”八哥进一步问道

“我觉得我可以试试,按照前面思路应该有迹可循”
罗拉想了一下
“根据之前的状态转换可知”

dp[i][0][0] = 0;
dp[i][0][1] = Math.max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
dp[i][0][2] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][0] - prices[i]);
dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][1] - prices[i]);
dp[i][1][2] = 0;

“虽然有六个状态,真正决定最后利润的只有四个状态”
“分别为dp[i][0][1]、dp[i][0][2],dp[i][1][0],dp[i][1][1]
“我们可以把这个四个状态用一个变量表示当前状态的最大利润,如下:”

状态 变量 含义 状态转换 备注
dp[i][1][0] fstBuy 第一次买 max(fstBuy, -price) 此时可能情况为:
1. 之前买了第一次
2. 现在买第一次
dp[i][0][1] fstSell 第一次卖 max(fstSell, fstBuy + price) 此时可能情况为:
1. 之前就卖了第一次
2. 现在第一次卖
dp[i][1][1] secBuy 第二次买 max(secBuy, fstSell - price) 此时可能情况为:
1. 之前买了第二次
2. 现在第二次买
dp[i][0][2] secSell 第二次卖 max(secSell, secBuy + price) 此时可能情况为:
1. 之前已经卖了第二次
2. 现在才第二次卖

“此时的代码实现如下:”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {3, 3, 5, 0, 0, 3, 1, 4};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading5(prices));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading5(pricesUp));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading5(pricesDown));
    }

    public static int stockTrading5(int[] prices) {
        //注意第一次卖和第二次卖的初始值,一定要比prices[0]小
        int fstBuy = Integer.MIN_VALUE, fstSell = 0;
        int secBuy = Integer.MIN_VALUE, secSell = 0;
        for (int price : prices) {
            //第一次买:要么之前买过,要么现在买
            fstBuy = Math.max(fstBuy, -price);
            //第一次卖,要么之前就卖了,要么现在第一次卖
            fstSell = Math.max(fstSell, fstBuy + price);
            //第二次买:要么之前买了,要么现在第二次买
            secBuy = Math.max(secBuy, fstSell - price);
            //第二次卖:要么之前已经卖了,要么现在才第二次卖
            secSell = Math.max(secSell, secBuy + price);
        }
        return secSell;
    }
}
//输出结果
prices 不限次交易最大利润为: 6
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

“你看看”

“嗯,不错,看来循序渐进还是很不错的,如果一开始直接给你这个估计你就蒙逼了”

“确实,有前面的打底,思路比较清晰,如果直接上来就是这个,老实说毫无思路。”罗拉爽快承认
“话说还有一个吧,看看最后一个能不能做出来。”罗拉做了几个,热情上来了,有点难以阻挡

“好,还有最后一个,请看题”


买卖股票的最佳时机(k次交易)

“第六个题目是:”

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

“完整的k次交易为:”

完整的k次交易为

“请问现在要如何做?”

“这...,字面意义告诉我,这个是第五个拓展,可以在第五个的基础上想想。” 罗拉想了一会道

“确实,不过能不能想出来就是另一个问题了”

“我试试”
“对于每一天来说,我只有两个状态,持有或者不持有”
“但是我们现在因为交易次数k的限制,我们必须要考虑每一次交易的状态”
“所以我们可以增加一个唯独来描述现在是第几次交易”
“对此可以通过dp[卖出的次数][当前是否持股]=dp[k][i]来记录状态”
“其中i={0,1};0表示卖出,1表示持有
“相应的状态转换也可以列出来,如下表”

状态 含义 状态转换 备注
dp[i][0] 第i次不持有 max(dp[i][0],dp[i][1]+price) 此时可能情况为:
1. 本来不持有,这次不操作
2. 第i次持有现在卖出
dp[i][1] 第i次持有 max(dp[i][1],dp[i-1][0]-price) 此时可能情况为:
1. 本来持有,这次不操作
2. 前一次不持有,现在买入

“那先在就可以写出相应的代码了”

public class StockTrading {
    public static void main(String[] args) {
        int[] prices = {3, 3, 5, 0, 0, 3, 1, 4};
        int[] pricesUp = {2, 3, 4, 5, 6, 7, 8, 9};
        int[] pricesDown = {9, 8, 7, 6, 5, 4, 3, 2};
        System.out.println("prices 不限次交易最大利润为: " + stockTrading6(prices, 2));
        System.out.println("pricesUp 不限次交易最大利润为: " + stockTrading6(pricesUp, 7));
        System.out.println("pricesDown 不限次交易最大利润为: " + stockTrading6(pricesDown, 7));
    }

    public static int stockTrading6(int[] prices, int k) {
        //如果交易次数小于1,返回0
        if (k < 1) return 0;
        //如果交易次数大于等于数组长度,此时就是第二种情况
        if (k >= prices.length / 2) return stockTrading2(prices);
        //每一天只有两个状态:买入和卖出
        //但是我们需要考虑次数k限制,所以我们可以增加一个维度描述第几次交易
        //dp[卖出的次数][当前是否持股]=dp[k][i],其中1={0,1};0表示卖出,1表示持有
        //此时只有两种状态:
        //1.第i次不持有:此时情况为:本来不持有,这次不操作;要么第i次持有现在卖出
        //dp[i][0] = (dp[i][0],dp[i][1]+price)
        //2.第i次持有:此时情况为:本来持有,这次不操作;要么前一次不持有持有现在买入
        //dp[i][1] = (dp[i][1],dp[i-1][0]-price)
        int[][] dp = new int[k][2];
        //边界值:初始持有的最小值一定要小于prices的最小值
        for (int i = 0; i < k; i++) dp[i][1] = Integer.MIN_VALUE;
        for (int price : prices) {
            //注意要重设第一次交易的初始值,否则存在某一天多次交易问题
            //第一次不持有:要么之前就不持有,此时不操作;要么之前持有,现在第一次卖出入
            dp[0][0] = Math.max(dp[0][0], dp[0][1] + price);
            //第一次持有: 要么之前就是第一次持有,此时不操作;要么之前不持有,现在第一次买入
            dp[0][1] = Math.max(dp[0][1], -price);
            for (int i = 1; i < k; i++) {
                dp[i][0] = Math.max(dp[i][0], dp[i][1] + price);
                dp[i][1] = Math.max(dp[i][1], dp[i - 1][0] - price);
            }
        }
        return dp[k - 1][0];
    }

    public static int stockTrading2(int[] prices) {
        if (prices == null || prices.length < 2) return 0;
        int sell = 0, buy = -prices[0], tmp = 0;
        for (int i = 1; i < prices.length; i++) {
            tmp = sell;
            sell = Math.max(sell, buy + prices[i]);
            buy = Math.max(buy, tmp - prices[i]);
        }
        return sell;
    }
}
//输出结果:
prices 不限次交易最大利润为: 6
pricesUp 不限次交易最大利润为: 7
pricesDown 不限次交易最大利润为: 0

此处需要注意去掉一天多次交易的问题,
这个可以通过逆序内循环解决,也可以通过每次重复初始化第一天状态解决。

“怎样,结果没错吧”罗拉得意道

“确实,很不错了,我还以为你会用三个dp[天数][是否持有股票][第k次交易]的方式来做,比我预想的好。”八哥感慨。

“一开始确实这么想,但是毕竟前面也有过有过优化方案,就想着直接优化后的方案看看能不能写出来,看来还挺顺利的。”
“这个还能优化嘛?”罗拉疑惑道。

“学无止境,应该还有优化的空间吧,不过我目前也没想到比你现在更好的方法吧”

“哎,要是我炒股也能这样,富婆梦早就实现了”

“得了吧,把股票价格都列出来来给你,谁还炒股...”八哥无限鄙视罗拉。

“先打住,出去吃饭吧,今晚跨年呢”

“行,走吧,去送别2020”

现在是20201231号,提前祝大家元旦快乐。

本文为原创文章,转载请注明出处!!!

欢迎关注【兔八哥杂谈】

推荐阅读