前言
今天王子與大家分享的是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道關於股票買賣的算法題我們就完美解決了,小伙伴們看懂了嗎,希望大家仔細思考解題思路,能實際運用這套框架哦,這是關於股票買賣算法的第一篇文章,后續會有補充內容,對剩下比較復雜的題目提供解題方法,歡迎閱讀我的下一篇文章,一起研究算法吧。
往期文章推薦:
中間件專輯:
算法專輯: