演算法系列-動態規劃(4):買賣股票的最佳時機

lillcol發表於2020-12-31

此係列為動態規劃相關文章。

系列歷史文章:
演算法系列-動態規劃(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號,提前祝大家元旦快樂。

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

歡迎關注【兔八哥雜談】

相關文章