算法系列-動態規劃(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號,提早祝你們元旦快樂。

本文爲原創文章,轉載請註明出處!!!

歡迎關注【兔八哥雜談】

相關文章
相關標籤/搜索