詳解股票買賣演算法的最優解(一)

王子發表於2020-09-08

前言

今天王子與大家分享的是LeeCode上有關如何買賣股票獲取最高利潤的題目。

主要用的技巧是“狀態機”,那麼什麼是“狀態機”呢?沒聽過的小夥伴會覺得它很高大尚,但今天我們討論過後,你會發現其實它就是那麼回事。

接下來,我們就以下邊的題目為基礎,講解一下“狀態機”是什麼。

請看題:

 

 

 看完題目後是不是覺得無從下手呢,沒關係,接下來我們進入正題。

 

窮舉框架

首先我們會想到,要解決這個問題需要怎麼進行窮舉,獲取出最大的利潤呢?要窮舉的物件又是什麼呢?

既然我們選擇了狀態機,那麼要窮舉的物件就是是狀態,窮舉狀態的一種框架就是下邊的模式:

for 狀態1 in 狀態1的所有取值
    for 狀態2 in 狀態2的所有取值
        for ...
            dp[狀態1][狀態2][...] = 擇優(選擇1,選擇2,...)

具體到我們的題目,分析可以知道我們每天都有三種選擇:買入、賣出、臥倒不動,我們用buy、sell、rest表示這三種選擇。

在此基礎上,我們再次分析,可以知道每天是不能隨意選擇這三種選擇的,它的選擇是有限制條件的。

sell必須要在buy之後,buy又必須在sell之後(除了第一次)。而對於rest又有兩種情況,如果是在buy之後rest,那麼目前就是持倉狀態,如果是在sell之後rest,那麼目前就是空倉狀態。而且我們還有一個買入次數K的限制,所以我們的buy是有限制的,buy<k, k>0。

分析題目,這個問題有三種狀態,第一個是天數,第二是允許交易的最大次數k,第三個是當前的持有狀態(空倉還是持倉,我們假設空倉為0,持倉為1)

看起來還可以理解吧,那麼如何窮舉呢?

我們用一個三維陣列dp就可以儲存這幾種狀態的全部組合,然後就可以用for迴圈完成窮舉,如下:

dp[i][k][0 or 1]
0<=i<=n-1,1<=k<=K
//n為天數,K為最多交易次數,全部窮舉如下

for 0<=i<n;
    for 1<=k<=K;
        for s in {0,1};
            dp[i][k][s] = max(buy,sell,rest)

如果上邊的表示式還是沒有看明白,那麼我們可以用大白話描述出每一個狀態的含義,比如說dp[3][2][1] 的含義就是:今天是第三天,我現在手上持有著股票,至今進行 2 次交易。再比如 dp[2][3][0] 的含義:今天是第二天,我現在手上沒有持有股票,至今進行 3 次交易。這樣就更容易理解了吧。

我們想要的最大利潤值一定是 dp[n - 1][k][0],也就是最後一天,交易了k次,空倉狀態。為什麼說是空倉狀態利潤最大呢,可以這麼理解,假設我們手上一共就這麼多錢用於買賣股票,不考慮利潤的情況下,如果買入股票變為持倉狀態,可以看成是我們的總資金減去了買入的資金,實際上我們的資金是變少的,而賣出變為空倉狀態,可以看成是我們把買入的資金又以不同的價格賣了出去,此時我們的總資金才真的增加了錢數,對於我們的總資金來說才算真正的盈利了。這其實就是我們平時理財的一個道理,如果買入了股票或基金,只要不賣出,你就不會真正的盈利,同樣也不會真正的虧損,好了這是題外話,之後會有理財專輯專門談談理財,我們迴歸正題。

狀態轉移框架

我們知道了有多少狀態,有多少選擇,那麼現在我們就開始考慮每種狀態有哪種選擇,他們之間如何組合。

三種選擇buy、sell、rest是隻與持有狀態相關的,所以可以畫出一個狀態轉移圖如下:

 

 

 通過這個圖我們可以清楚的看到0和1之間是如何因為選擇而轉換的。可以寫出狀態轉移方程如下:

dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i])
                  max(    選擇rest,       選擇sell       )    
解釋:今天沒有持有股票有兩種情況
        昨天沒有持有,今天沒有操作,所以今天沒有持有
        昨天持有股票,今天賣出操作,所以今天沒有持有

dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i])
                  max(    選擇rest,       選擇buyl       )    
解釋:今天持有股票有兩種情況
        昨天持有股票,今天沒有操作,所以今天持有股票
        昨天沒有持有,今天買入操作,所以今天持有股票

轉移方程中的解釋應該很清楚了,如果buy就從總錢數裡減去prices[i],如果sell就給總錢數加上prices[i]。那麼今天的最大利潤就是在這兩種選擇中選取總錢數最大的那種情況。而且要保證交易次數的限制,buy了一次k就增加1,保證k<K。

 

現在我們已經把解題的核心部分,狀態轉移方程寫完了,那麼對於題目其實就是套用框架了,不過在套用之前,我們先把一些特殊情況考慮進去。

dp[-1][k][0] = 0;
// 因為i>0,所以i=-1代表還沒開始,利潤為0

dp[-1][k][1] = 不存在;
// 還沒開始是不可能持有股票的

dp[i][0][0] = 0;
// k=0代表還沒交易過,利潤當然是0

dp[i][0][1] = 不存在;
// k=0代表還沒交易過,不可能持有股票

 

解決題目

第一題:k=1,即最多完成一次交易

直接套用框架如下:

dp[i][1][0] = max(dp[i-1][1][0],dp[i-1][1][1]+prices[i]);
dp[i][1][1] = max(dp[i-1][1][1],dp[i-1][0][0]-prices[i])
                 =  max(dp[i-1][1][1],-prices[i]);
// 因為dp[i-1][0][0]=0

// 可以發現k都是1,也就是說k不影響狀態轉移,所以可以簡化如下:
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][1],-prices[i]);

同時我們還要考慮當i=0的時候,情況特殊,所以我們可以單獨設定變數儲存特殊情況

翻譯成最終程式碼如下:

    public int maxProfit(int[] prices) {
        int n=prices.length;
        int dp_i_0=0,dp_i_1=Integer.MIN_VALUE;
        for(int i=0;i<n;i++){
            dp_i_0=Math.max(dp_i_0,dp_i_1+prices[i]);
            dp_i_1=Math.max(dp_i_1,-prices[i]);
        }
        return dp_i_0;
    }

相信小夥伴們前邊的內容理解清楚後,最終的程式碼是能夠看懂的,我們繼續看下一題。

第二題,k=+infinity,及不限制交易次數

如果k=+infinity,那麼就可以認為k-1=k,所以可以引入改寫框架如下:

dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]);
dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i])
                 =  max(dp[i-1][k][1],dp[i-1][k][0]-prices[i]);

// 可以發現k值全部相同,也就是說k不影響狀態轉移,所以可以簡化如下:
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]);

直接翻譯成程式碼如下:

    public int maxProfit(int[] prices) {
        int n=prices.length;
        int dp_i_0=0,dp_i_1=Integer.MIN_VALUE;
        for(int i=0;i<n;i++){
            int temp=dp_i_0;
            dp_i_0=Math.max(dp_i_0,dp_i_1+prices[i]);
            dp_i_1=Math.max(dp_i_1,temp-prices[i]);
        }
        return dp_i_0;
    }    

第三題,k=+infinity ,而且帶有冷凍期

解釋一下題目,就是,賣出股票後,你無法在第二天買入股票 (即冷凍期為 1 天)。不限制交易次數

狀態轉移方程如下:

dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-2][0]-prices[i]);
// 第i天選擇buy的時候,要從i-2的狀態轉移(冷凍期1天)

直接翻譯成程式碼如下:

    public int maxProfit(int[] prices) {
        int n=prices.length;
        int dp_i_0=0,dp_i_1=Integer.MIN_VALUE;
        int dp_pre_0=0;//代表dp[i-2][0]
        for(int i=0;i<n;i++){
            int temp=dp_i_0;
            dp_i_0=Math.max(dp_i_0,dp_i_1+prices[i]);
            dp_i_1=Math.max(dp_i_1,dp_pre_0-prices[i]);
            dp_pre_0=temp;
        }
        return dp_i_0;
    }    

第四題,k=+infinity,帶手續費

手續費題目中這樣描述:這裡的一筆交易指買入持有並賣出股票的整個過程,每筆交易你只需要為支付一次手續費。

那麼狀態轉移方程中,我們每次賣出的時候,把手續費減掉就可以了,如下:

dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]-fee);
// fee代表手續費,兩個式子裡隨便一個減掉一次就可以了,可以看成是買入的時候交手續費或者賣出的時候交手續費

直接翻譯成程式碼如下:

    public int maxProfit(int[] prices,int fee) {
        int n=prices.length;
        int dp_i_0=0,dp_i_1=Integer.MIN_VALUE;
        for(int i=0;i<n;i++){
            int temp=dp_i_0;
            dp_i_0=Math.max(dp_i_0,dp_i_1+prices[i]);
            dp_i_1=Math.max(dp_i_1,temp-prices[i]-fee);
        }
        return dp_i_0;
    }    

總結

好了,看到這裡以上4道關於股票買賣的演算法題我們就完美解決了,小夥伴們看懂了嗎,希望大家仔細思考解題思路,能實際運用這套框架哦,這是關於股票買賣演算法的第一篇文章,後續會有補充內容,對剩下比較複雜的題目提供解題方法,歡迎閱讀我的下一篇文章,一起研究演算法吧。

 

往期文章推薦:

中介軟體專輯:

什麼是訊息中介軟體?主要作用是什麼?

常見的訊息中介軟體有哪些?你們是怎麼進行技術選型的?

你懂RocketMQ 的架構原理嗎?

聊一聊RocketMQ的註冊中心NameServer

Broker的主從架構是怎麼實現的?

演算法專輯:

和同事談談Flood Fill 演算法

相關文章