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

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

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

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

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

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


新生韭菜罗拉

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

因而她瞄上了股票,做为股场新人,罗拉但是满怀信心的。
以为本身只要顺应潮流,识大致,懂进退,不贪心,即便不赚大钱,也不至于亏钱。 因此她想拿个一千八百试试水。blog

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

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

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

对话记录

八哥

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

罗拉

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

八哥

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

罗拉

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

八哥

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

罗拉

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

八哥

是的
因此我一开始也不劝你,毕竟你们开始的心态和你都差很少。
只有被割过韭菜,才会知道炒股高风险,高回报。不是通常人能玩的。

罗拉

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

八哥

富婆梦可能毫无收获,可是这个经历到时能够用来提高一下本身,
买卖股票但是一个很经典的算法题哦。
固然这个过后诸葛亮的题目。

罗拉

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

八哥

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

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

八哥韭菜曲线


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

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

“好,最近七八天的价格是这样的:{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号,提早祝你们元旦快乐。

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

欢迎关注【兔八哥杂谈】

相关文章
相关标签/搜索