leetcode股票問題方法收集 轉載自微信公衆號labuladong

1、窮舉框架
首先,仍是同樣的思路:如何窮舉?這裏的窮舉思路和上篇文章遞歸的思想不太同樣。算法

遞歸實際上是符合咱們思考的邏輯的,一步步推動,遇到沒法解決的就丟給遞歸,一不當心就作出來了,可讀性還很好。缺點就是一旦出錯,你也不容易找到錯誤出現的緣由。好比上篇文章的遞歸解法,確定還有計算冗餘,但確實不容易找到。編程

而這裏,咱們不用遞歸思想進行窮舉,而是利用「狀態」進行窮舉。咱們具體到每一天,看看總共有幾種可能的「狀態」,再找出每一個「狀態」對應的「選擇」。咱們要窮舉全部「狀態」,窮舉的目的是根據對應的「選擇」更新狀態。聽起來抽象,你只要記住「狀態」和「選擇」兩個詞就行,下面實操一下就很容易明白了。數組

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 還只能在 k > 0 的前提下操做。框架

很複雜對吧,不要怕,咱們如今的目的只是窮舉,你有再多的狀態,老夫要作的就是一把梭所有列舉出來。這個問題的「狀態」有三個,第一個是天數,第二個是容許交易的最大次數,第三個是當前的持有狀態(即以前說的 rest 的狀態,咱們不妨用 1 表示持有,0 表示沒有持有)。而後咱們用一個三維數組就能夠裝下這幾種狀態的所有組合:翻譯

dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 爲天數,大 K 爲最多交易數
此問題共 n × K × 2 種狀態,所有窮舉就能搞定。rest

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 次交易。很容易理解,對吧?code

咱們想求的最終答案是 dp[n - 1][K][0],即最後一天,最多容許 K 次交易,最多得到多少利潤。讀者可能問爲何不是 dp[n - 1][K][1]?由於 [1] 表明手上還持有股票,[0] 表示手上的股票已經賣出去了,很顯而後者獲得的利潤必定大於前者。遞歸

記住如何解釋「狀態」,一旦你以爲哪裏很差理解,把它翻譯成天然語言就容易理解了。索引

2、狀態轉移框架
如今,咱們完成了「狀態」的窮舉,咱們開始思考每種「狀態」有哪些「選擇」,應該如何更新「狀態」。只看「持有狀態」,能夠畫個狀態轉移圖。內存

 

經過這個圖能夠很清楚地看到,每種狀態(0 和 1)是如何轉移而來的。根據這個圖,咱們來寫一下狀態轉移方程:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 選擇 rest , 選擇 sell )

解釋:今天我沒有持有股票,有兩種可能:
要麼是我昨天就沒有持有,而後今天選擇 rest,因此我今天仍是沒有持有;
要麼是我昨天持有股票,可是今天我 sell 了,因此我今天沒有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 選擇 rest , 選擇 buy )

解釋:今天我持有着股票,有兩種可能:
要麼我昨天就持有着股票,而後今天選擇 rest,因此我今天還持有着股票;
要麼我昨天本沒有持有,但今天我選擇 buy,因此今天我就持有股票了。
這個解釋應該很清楚了,若是 buy,就要從利潤中減去 prices[i],若是 sell,就要給利潤增長 prices[i]。今天的最大利潤就是這兩種可能選擇中較大的那個。並且注意 k 的限制,咱們在選擇 buy 的時候,把 k 減少了 1,很好理解吧,固然你也能夠在 sell 的時候減 1,同樣的。

如今,咱們已經完成了動態規劃中最困難的一步:狀態轉移方程。若是以前的內容你均可以理解,那麼你已經能夠秒殺全部問題了,只要套這個框架就好了。不過還差最後一點點,就是定義 base case,即最簡單的狀況。

dp[-1][k][0] = 0
解釋:由於 i 是從 0 開始的,因此 i = -1 意味着尚未開始,這時候的利潤固然是 0 。
dp[-1][k][1] = -infinity
解釋:還沒開始的時候,是不可能持有股票的,用負無窮表示這種不可能。
dp[i][0][0] = 0
解釋:由於 k 是從 1 開始的,因此 k = 0 意味着根本不容許交易,這時候利潤固然是 0 。
dp[i][0][1] = -infinity
解釋:不容許交易的狀況下,是不可能持有股票的,用負無窮表示這種不可能。
把上面的狀態轉移方程總結一下:

base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity

狀態轉移方程:
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])
讀者可能會問,這個數組索引是 -1 怎麼編程表示出來呢,負無窮怎麼表示呢?這都是細節問題,有不少方法實現。如今完整的框架已經完成,下面開始具體化。

3、秒殺題目
第一題,k = 1

直接套狀態轉移方程,根據 base case,能夠作一些化簡:

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])
解釋:k = 0 的 base case,因此 dp[i-1][0][0] = 0。

如今發現 k 都是 1,不會改變,即 k 對狀態轉移已經沒有影響了。
能夠進行進一步化簡去掉全部 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])
直接寫出代碼:

int n = prices.length;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];
顯然 i = 0 時 dp[i-1] 是不合法的。這是由於咱們沒有對 i 的 base case 進行處理。能夠這樣處理:

for (int i = 0; i < n; i++) {
if (i - 1 == -1) {
dp[i][0] = 0;
// 解釋:
// dp[i][0]
// = max(dp[-1][0], dp[-1][1] + prices[i])
// = max(0, -infinity + prices[i]) = 0
dp[i][1] = -prices[i];
//解釋:
// dp[i][1]
// = max(dp[-1][1], dp[-1][0] - prices[i])
// = max(-infinity, 0 - prices[i])
// = -prices[i]
continue;
}
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];
第一題就解決了,可是這樣處理 base case 很麻煩,並且注意一下狀態轉移方程,新狀態只和相鄰的一個狀態有關,其實不用整個 dp 數組,只須要一個變量儲存相鄰的那個狀態就足夠了,這樣能夠把空間複雜度降到 O(1):

// k == 1
int maxProfit_k_1(int[] prices) {
int n = prices.length;
// base case: dp[-1][0] = 0, dp[-1][1] = -infinity
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
// dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
// dp[i][1] = max(dp[i-1][1], -prices[i])
dp_i_1 = Math.max(dp_i_1, -prices[i]);
}
return dp_i_0;
}
兩種方式都是同樣的,不過這種編程方法簡潔不少。可是若是沒有前面狀態轉移方程的引導,是確定看不懂的。後續的題目,我主要寫這種空間複雜度 O(1) 的解法。

第二題,k = +infinity

若是 k 爲正無窮,那麼就能夠認爲 k 和 k - 1 是同樣的。能夠這樣改寫框架:

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])
直接翻譯成代碼:

int maxProfit_k_inf(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 with cooldown

每次 sell 以後要等一天才能繼續交易。只要把這個特色融入上一題的狀態轉移方程便可:

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 的狀態轉移,而不是 i-1 。
翻譯成代碼:

int maxProfit_with_cool(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 with fee

每次交易要支付手續費,只要把手續費從利潤中減去便可。改寫方程:

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)
解釋:至關於買入股票的價格升高了。
在第一個式子裏減也是同樣的,至關於賣出股票的價格減少了。
直接翻譯成代碼:

int maxProfit_with_fee(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;
}
第五題,k = 2

k = 2 和前面題目的狀況稍微不一樣,由於上面的狀況都和 k 的關係不太大。要麼 k 是正無窮,狀態轉移和 k 不要緊了;要麼 k = 1,跟 k = 0 這個 base case 捱得近,最後也沒有存在感。

這道題 k = 2 和後面要講的 k 是任意正整數的狀況中,對 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])
按照以前的代碼,咱們可能想固然這樣寫代碼(錯誤的):

int k = 2;
int[][][] dp = new int[n][k + 1][2];
for (int i = 0; i < n; i++)
if (i - 1 == -1) { /* 處理一下 base case*/ }
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[n - 1][k][0];
爲何錯誤?我這不是照着狀態轉移方程寫的嗎?

還記得前面總結的「窮舉框架」嗎?就是說咱們必須窮舉全部狀態。其實咱們以前的解法,都在窮舉全部狀態,只是以前的題目中 k 都被化簡掉了。這道題因爲沒有消掉 k 的影響,因此必需要對 k 進行窮舉:

int max_k = 2;
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++) {
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) { /*處理 base case */ }
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]);
}
}
// 窮舉了 n × max_k × 2 個狀態,正確。
return dp[n - 1][max_k][0];
若是你不理解,能夠返回第一點「窮舉框架」從新閱讀體會一下。

這裏 k 取值範圍比較小,因此能夠不用 for 循環,直接把 k = 1 和 2 的狀況手動列舉出來也能夠:

dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
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], -prices[i])

int maxProfit_k_2(int[] prices) {
int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
for (int price : prices) {
dp_i20 = Math.max(dp_i20, dp_i21 + price);
dp_i21 = Math.max(dp_i21, dp_i10 - price);
dp_i10 = Math.max(dp_i10, dp_i11 + price);
dp_i11 = Math.max(dp_i11, -price);
}
return dp_i20;
}
有狀態轉移方程和含義明確的變量名指導,相信你很容易看懂。其實咱們能夠故弄玄虛,把上述四個變量換成 a, b, c, d。這樣當別人看到你的代碼時就會一頭霧水,大驚失色,不得不對你肅然起敬。

第六題,k = any integer

有了上一題 k = 2 的鋪墊,這題應該和上一題的第一個解法沒啥區別。可是出現了一個超內存的錯誤,原來是傳入的 k 值會很是大,dp 數組太大了。如今想一想,交易次數 k 最多有多大呢?

一次交易由買入和賣出構成,至少須要兩天。因此說有效的限制 k 應該不超過 n/2,若是超過,就沒有約束做用了,至關於 k = +infinity。這種狀況是以前解決過的。

直接把以前的代碼重用:

int maxProfit_k_any(int max_k, int[] prices) {
int n = prices.length;
if (max_k > n / 2)
return maxProfit_k_inf(prices);

int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++)
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) { /* 處理 base case */ }
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]);
}
return dp[n - 1][max_k][0];
}
至此,6 道題目經過一個狀態轉移方程所有解決。

4、最後總結
本文給你們講了如何經過狀態轉移的方法解決複雜的問題,用一個狀態轉移方程秒殺了 6 道股票買賣問題,如今想一想,其實也不算難對吧?這已經屬於動態規劃問題中較困難的了。

關鍵就在於列舉出全部可能的「狀態」,而後想一想怎麼窮舉更新這些「狀態」。通常用一個多維 dp 數組儲存這些狀態,從 base case 開始向後推動,推動到最後的狀態,就是咱們想要的答案。想一想這個過程,你是否是有點理解「動態規劃」這個名詞的意義了呢?

具體到股票買賣問題,咱們發現了三個狀態,使用了一個三維數組,無非仍是窮舉 + 更新,不過咱們能夠說的高大上一點,這叫「三維 DP」,怕不怕?這個大實話一說,馬上顯得你高人一等,名利雙收有沒有。

因此,你們不要被各類高大上的名詞嚇到,再多的困難問題,奇技淫巧,也不過是基本套路的不斷升級組合產生的。只要把住算法的底層原理,便可觸類旁通,逐個擊破。

做者:labuladong連接:https://leetcode-cn.com/problems/two-sum/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-l-3/來源:力扣(LeetCode)著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索