導言:java
股票買賣問題是 LeetCode 算法題當中的一個系列問題,主要的考查點就是動態規劃,可是若是針對這裏的每道題都去思考和總結,其實獲得的解法不具備通常性,這篇文章想要作的就是針對這一系列問題提煉出一個通用解法算法
股票買賣這一類的問題,都是給一個輸入數組,表示的是天天的股價,而且限定你手頭不能存有多於 1 支股,也就是手上有股票的時候必須賣掉才能再繼續賣,並且只能買一支,通常來講有下面幾種問法:數組
固然還有一些變種,例如每次買賣都有交易費,另外還有就是交易事後必需隔一天再繼續買賣。bash
這類題目當中的頭兩題,就是隻能交易一次,和能夠交易無數次是能夠根據常識來解決的,只能交易一次無非就是遍歷數組記錄當前通過的最小值,而後用當前值減去最小值去記錄差價,去差價最大的便可。只能交易無數次也是遍歷數組,只要當前的值 比以前的存在的值大,就累加差價,而且把當前遍歷到的值設置成 「以前的值」,而後繼續遍歷下去。框架
這裏主要考慮 k 次交易的狀況,固然這裏的框架稍做調整也是能夠用到只能交易一次和能夠交易無數次的題目中去的,最好的解決方式確定是動態規劃,可是關鍵在於狀態怎麼定義,動態規劃的方程怎麼寫。咱們能夠思考得出題目當中的變量有如下幾個:優化
其中 「當前可得到的最大利潤」 就是咱們最後要求解的值,那麼這麼看來 DP 數組的值能夠試着用來表示 「當前可得到的最大利潤」 ,而後接着看,因爲給定了輸入數組,「股票的價格」 是和 「第幾天」 綁定在一塊兒的,這樣 DP 的狀態實際上是由 「第幾天」,「手頭有無股票」,還有 「第幾回交易」 來決定的,那咱們就能夠獲得 DP 的狀態:spa
DP[i][j][0] -> 第 i 天,第 j 次交易,手頭沒有股票的最大利潤
DP[i][j][1] -> 第 i 天,第 j 次交易,手頭有股票的最大利潤
複製代碼
這樣,咱們要求解的答案就是:code
Max(DP[n][0][0], DP[n][1][0], ..., DP[n][k][0])
複製代碼
這裏補充一點就是,手頭有股票的狀況確定不會是最後的答案,由於股票的價格都是正數,買股票是要花錢的,即 DP[i][j][0] - prices[a] < DP[i][j][0]
leetcode
另一個問題就是動歸的遞推方程怎麼寫,由於當前的 DP 狀態只會和它以前的狀態相關,並不會被後面的狀態所影響,並且在思考的過程當中要有一個認識就是,以前的 DP 值全是局部的最優解,所以,咱們能夠思考一個問題是 「當前的最大利潤和前面哪些狀態相關?」,而後你會發現每一個狀態只會和它相鄰的狀態影響,也就是第 i 天的最大利潤能夠經過第 i - 1 天的最大利潤求解,第 k 次交易的最大利潤能夠經過第 k - 1 次交易的最大利潤求解,另外就是手頭有無股票也是能夠經過買入和賣出相互轉化的,所以咱們能夠得出遞推方程以下:get
DP[i][k][0] = Max(DP[i - 1][k][0], DP[i - 1][k - 1][1] + a[i])
DP[i][k][1] = Max(DP[i - 1][k][1], DP[i - 1][k - 1][0] - a[i])
複製代碼
有了 DP 的狀態定義,和遞推方程,剩下的工做就是寫代碼了,應該問題已經基本解決了。
LeetCode 121. Best Time to Buy and Sell Stock
這道題比較簡單,套進咱們以前總結解法也是能夠很好解決的,k = 1,這裏 DP 數組開 2 是爲了來表示沒有交易和交易 1 次:
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int[][][] dp = new int[prices.length][2][2];
dp[0][0][0] = 0; dp[0][0][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][1] + prices[i]);
dp[i][0][1] = Math.max(dp[i - 1][0][1], dp[i - 1][0][0] - prices[i]);
}
return dp[prices.length - 1][1][0];
}
複製代碼
LeetCode 122. Best Time to Buy and Sell Stock II
由於這題的交易次數不受限制,也就是當前進行買和賣都是能夠的,不用考慮前面的 k - 1 次的狀態,所以咱們就沒必要開多一維數組
public int maxProfit(int[] prices) {
if ((prices == null) || (prices.length < 2)) {
return 0;
}
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; ++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], dp[i - 1][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
複製代碼
LeetCode 123. Best Time to Buy and Sell Stock III
這道題符合咱們以前講到的框架,其中 k = 2,所以根據 DP 狀態中的交易次數,咱們開長度爲 3 的 DP 數組,分別用來表示第 0 次交易,第 1 次交易,第 2 次交易,其中 dp[i][0][0] 是不用根據前面的狀態來更新的,其值永遠等於零,即 dp[0][0][0] = dp[1][0][0] = ... = dp[n - 1][0][0]
,還有 dp[i][2][1] 也是不須要考慮的,由於交易次數限定爲最高 2 次,不可能存在第三次交易的狀況
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
// dp[i][j][k] -> day, time, whether have stock or not
int[][][] dp = new int[prices.length][3][2];
dp[0][0][1] = -prices[0]; dp[0][1][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][0][1] = Math.max(dp[i - 1][0][0] - prices[i], dp[i - 1][0][1]);
dp[i][1][0] = Math.max(dp[i - 1][0][1] + prices[i], dp[i - 1][1][0]);
dp[i][1][1] = Math.max(dp[i - 1][1][0] - prices[i], dp[i - 1][1][1]);
dp[i][2][0] = Math.max(dp[i - 1][1][1] + prices[i], dp[i - 1][2][0]);
}
return dp[prices.length - 1][2][0];
}
複製代碼
LeetCode 188. Best Time to Buy and Sell Stock IV
這題就是咱們以前討論當中的案例,可是考慮到第 0 次交易的狀況是無法根據前面的 DP 的值來計算的,爲來計算的方便,把第 0 次交易的狀況單獨挪出第二層循環進行處理,還有就是,LeetCode 給了一個很是極端的 testcase,就是 k 很是大,k >> prices.length
,爲了程序可以順利經過,這種狀況下就直接變成第二題的解法,這裏就直接按常識簡寫了:
public int maxProfit(int k, int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
if (k > prices.length / 2) {
int max = 0; int hold = prices[0];
for (int i = 1; i < prices.length; ++i) {
max += Math.max(0, prices[i] - prices[i - 1]);
}
return max;
}
// dp[i][j][0] -> at ith day, jth transaction, without stock in hand
// dp[i][j][1] -> at ith day, jth transaction, with stock in hand
int[][][] dp = new int[prices.length][k + 1][2];
// init
for (int i = 0; i <= k; ++i) {
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < prices.length; ++i) {
dp[i][0][1] = Math.max(dp[i - 1][0][1], dp[i - 1][0][0] - prices[i]);
for (int j = 1; j <= k; ++j) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j - 1][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j][0] - prices[i]);
}
}
return dp[prices.length - 1][k][0];
}
複製代碼
LeetCode 309. Best Time to Buy and Sell Stock with Cooldown
這道題在買賣不受限制題目的基礎上,加了條件,就是買賣後,必須等上至少一天才能繼續買賣,這樣的話狀態略微改變便可,就是在以前 「手頭有股票」 和 「手頭沒有股票」 兩種狀態的基礎上,多加一個 「冷卻」 這麼一個狀態;遞推方程跟以前不一樣的是,買股票的話,只能從前一天的 「冷卻」 狀態來決定,而不是 「手頭沒有股票」
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int[][] dp = new int[prices.length][3];
dp[0][0] = 0; dp[0][1] = -prices[0]; dp[0][2] = 0;
for (int i = 1; i < prices.length; ++i) {
dp[i][0] = Math.max(dp[i - 1][1] + prices[i], Math.max(dp[i - 1][0], dp[i - 1][2]));
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][0], dp[i - 1][2]);
}
return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][2]);
}
複製代碼
LeetCode 714. Best Time to Buy and Sell Stock with Transaction Fee
這道題也是在買賣不受限制題目的基礎上加上了 「每次交易都要交費用,每次費用相同」 這麼一個條件,那其實在買賣不受限制題目的基礎上,惟一須要改變的就是在賣票的時候減去交易費用便可,固然在買股票的時候減去這個交易費也是能夠的
public int maxProfit(int[] prices, int fee) {
if (prices == null || prices.length < 2) {
return 0;
}
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; ++i) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
複製代碼
以上六道題就是 LeetCode 當中股票系列的所有內容,固然這樣的設定 DP 狀態和定義 DP 遞推方程的思想,是能夠複用到其餘類型的 DP 問題當中去的,總的來講就是根據變量來定義 DP 數組當中存的值以及狀態,根據當前狀態和以前狀態的關係來肯定 DP 方程,這個須要平時的積累和大量的刷題練習。另外題目解答當中沒有提到的是,對於 DP 數組的空間優化,咱們能夠利用滾動數組來優化。