leetcode -[動態規劃、最大連續子數組] - 買賣股票的最佳時機(121)

1、問題描述

給定一個數組,數組中的第i個元素表示一支給定股票第i天的價格。
如果你最多隻許完成一筆交易,即只能買入一次和賣出一次,
設計一個算法來計算你能獲得的最大收益。
注意,你不能在買入股票前賣出股票。

示例1:

輸入:[7,1,2,5,3,6]
輸出:5
解釋:在第2天的時候買入(股票價格爲1),第6天的時候賣出(股票價格爲6),可以獲得最大收益6-1 = 5.

示例2:

輸入:[7,6,4,3,1]
輸出:0 。
解釋:當天買入,當天賣出不會出現虧損。

2、解題思路

解決這個問題有以下幾種方法:

  • 方法1:暴力法。使用指針i、j雙層循環,外部循環i表示在第i天的時候買入,內部循環j表示在第j天的時候就賣出,根據要求,j必須大於i,因此有i的遍歷範圍爲1到數組長度,j的遍歷範圍爲i+1到數組長度。很明顯,這種方法的時間複雜度爲O(n2)O(n^2),空間複雜度爲O(1)O(1)
  • 方法2:動態規劃。這個方法有兩個關鍵變量,maxprofit和minprice。
    maxpofit:表示到第j天之前獲得的最大收益;
    minprice:記錄第j天之前,股票的最低價格;
(1) j從頭開始遍歷價格數組prices:
	(2)如果在第j天賣出時獲得的收益大於maxprofit,則更新			maxprofit,即maxprofit = max(maxprofit,prices[j] - minprice);
	(3)如果第j天股票的價格prices[j]小於minprice,則更新minprices,即minprice =min(minprice,prices[j])

這種方法的時間複雜度爲O(n)O(n),空間複雜度爲O(1)O(1).

  • 方法3:轉換成求最大連續子數組的問題。
    求出價格數組相鄰兩天價格的差值,得到一個新的差值數組。
    然後在差值數組上求最大連續子數組的和即爲可獲得的最大收益。
    這種方法的基本思想是,假設你在重複這樣的操作,不斷地在第i天買入,第i+1天賣出,求累積的收益。
    這種方法的時間複雜度爲O(n)O(n),空間複雜度爲O(n)O(n).

3、代碼實現

方法2:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0 || prices.size() == 1){
            return 0;
        }
        int maxprofit = 0;
        int minprice = prices[0];
        for(int j = 0; j < prices.size(); j++){
            maxprofit = max(maxprofit, prices[j] - minprice);
            minprice = min(minprice, prices[j]);
        }
        return maxprofit;
    }
};

方法2:

/*方法1:
思想:轉換成求最大連續子數組的問題,在原數組上求相鄰兩個元素的差值得到一個新的數組,然後在新的數組上求最大連續子數組。
*/
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0 || prices.size() == 1){
            return 0;
        }
        vector<int> gain;
        for(int i = 0; i < prices.size() - 1; i++){
            gain.push_back(prices[i+1] - prices[i]);
        }
        int ngreatest = gain[0];
        int cursum = gain[0];
        for(int j = 1; j < gain.size(); j++){
            if(cursum < 0){
                cursum = gain[j];
                
            }
            else{
                cursum += gain[j];
            }
            
            if(cursum > ngreatest){
                ngreatest = cursum;
                
            }
        }
        
        ngreatest = max(ngreatest, 0);
        
        return ngreatest;


    }
};

4、拓展問題

拓展1: 如果允許多次交易,即多次買入和賣出一支股票,求能獲得的最大利潤。

問題鏈接:122. 買賣股票的最佳時機 II
分析:這一拓展問題的難點在於,要獲得最大利潤,交易的次數是未知的。解決這道問題有以下幾種方法:
方法1:暴力搜索。使用這種方法時,我們需要計算所有可能的交易組合相對應的最大利潤。交易組合的產生是通過在每一天,根據當天是否持有股票來選擇相應的操作(操作的種類包括不操作、買入、賣出)、交易組合所對應的解空間樹如下圖所示:
在這裏插入圖片描述
代碼實現

/*算法思想:
深度優先遍歷交易組合的解空間樹
*/
class Solution {
    int maxprofit;
public:

    int maxProfit(vector<int>& prices) {
        if(prices.size() < 2){
            return 0;
        }
        int status = 0;
        int curprofit = 0;
        // int maxprofit = 0;
        int day = 0;
        maxprofit = 0;

        dfs(prices, day,  status, curprofit);
        return maxprofit;

    }
    
    /*day:表示搜索的第幾天
      status: 表示當前是否持有股票;
      curprofit:搜索到當前節點的利益和;
      maxprofit: 最大利益*/
    void dfs(vector<int>& prices, int day, int status, int curprofit){
        // cout<<"day="<<day<<endl;
        if(day == prices.size()){
            maxprofit = max(maxprofit, curprofit);
            return ;
        }
        /*保持當前狀態,沒有股票仍然不買入,有股票不賣出*/
        dfs(prices, day + 1, status, curprofit);

        /*如果當天沒有股票,嘗試買入股票,否則嘗試賣出股票*/
        if(status == 0){
            dfs(prices, day + 1, 1, curprofit - prices[day]);
        }
        else{
            dfs(prices, day + 1, 0, curprofit + prices[day]);
        }

    }
};

方法2:動態規劃。
(1) 定義狀態:
根據交易組合的解空間樹,我們可以使用dp[i][j]來表示到第i天爲止,持有股票狀態爲j時的所獲得的最大收益。
i的取值範圍爲[0,prices.length - 1];
j的取值範圍爲{0, 1},1表示有股票,0表示沒有股票。
(2) 確定狀態轉移方程

  • 狀態從持有現金開始,到最後一天我們關心的狀態仍然是持有現金
  • 每一天的狀態可以保持不變,也可以轉移,狀態轉移用下圖表示:
    在這裏插入圖片描述
  • 因爲交易次數不受限制,因此除了最後一天,每一天都可能的狀態都可能保持不變,也可能轉移。
  • 經過以上分析:狀態轉移方程如下:
    dp[i][j]={max{dp[i1][0]dp[i1][1]+prices[i]}j=0;max{dp[i1][1]dp[i1][0]prices[i]}j=1;0i=0andj=0;prices[i]i=0andj=1;dp[i][j]=\begin{cases} max\{dp[i-1][0]、dp[i-1][1] + prices[i]\} & j =0; \\ max\{dp[i-1][1]、dp[i-1][0] - prices[i]\} & j =1; \\ 0 & i = 0 and j =0; \\ -prices[i] & i = 0 and j = 1; \end{cases}

(3)確定起始
起始的時候: 如果什麼都不做,dp[0][0]= 0;
如果買入股票,當前收益就是負數,dp[0][1]=-prices[0];

(4)確定終止
輸出dp[prices.length - 1][0]。
代碼實現

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2){
            return 0;
        }
        int dp[size][2];
        dp[0][0] = 0;
        dp[0][1] = 0 - prices[0];
        for(int i = 1; i < size; i++){
            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]);
        }

        return dp[size - 1][0];
    }
};

算法優化:
考慮狀態壓縮,由於當前行只參考上一行,而每一行只有兩個值,因此可以考慮滾動變量(“滾動數組”技巧)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2){
            return 0;
        }

        //持有股票獲得的最大收益
        int hold = - prices[0];
        //不持有股票獲得的最大收益
        int nothold = 0;
        for(int i = 1; i < size; i++){
        	int temp = hold;
            hold = max(hold, nothold - prices[i]);
            nothold = max(nothold, temp + prices[i]);
        }
        return nothold;

    }
};

方法3:貪心算法。這種方法的基本思想,在前後兩天,只要後一天的股票價格大於前一天,就在前一天買入,後一天賣出,不斷按照這種方式進行交易,收益就會不斷累積,最終達到最大。其示意圖如下所示:
在這裏插入圖片描述

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2){
            return 0;
        }
        int maxprofit = 0;
        for(int i = 1; i < size; i++){
            if(prices[i] > prices[i - 1]){
                maxprofit += prices[i] - prices[i - 1];
            }
        }
        return maxprofit;

    }
};

拓展問題2:假設你最多可以完成兩筆交易,求你能獲得的最大利潤。

問題鏈接:買賣股票的最佳時機 III
分析:這道題加了最多隻能進行兩次交易的約束。可以採用以下幾個方法:
方法1:劃分成兩次交易。對基本問題1進行改造,基本問題1是要求只能交易一次,而本問題是最多交易兩次,因此,可以把prices數組劃分成兩個不相交的子數組,設分別爲x和y,在子數組x上做一次交易,然後再子數組y上再做一次交易。對於不同的x、y劃分方式,找出兩次交易和的最大值。這種方法的時間複雜度爲O(n2)O(n^2).
代碼實現:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        if(size < 2){
            return 0;
        }

        int maxprofit = 0;
        for(int split = 0; split <= size; split++){
            int sumprofit = maxProfitHelper(prices,0,split) + maxProfitHelper(prices,split,size);
            if(sumprofit > maxprofit){
                maxprofit = sumprofit;
            } 
        }

        return maxprofit;


    }
    int maxProfitHelper(vector<int>& prices, int left, int right){
        if(left == right){
            return 0;
        }
        int maxprofit = 0;
        int minprice = prices[left];
        for(int i = left; i < right; i++){
            maxprofit = max(maxprofit, prices[i] - minprice);
            minprice = min(minprice, prices[i]);
        }
        return maxprofit;
    }
};

其他拓展問題:

3、

5、通用方法框架

這 6 道股票買賣問題是有共性的,我們通過對第四題(限制最大交易次數爲 k)的分析一道一道解決。因爲第四題是一個最泛化的形式,其他的問題都是這個形式的簡化。

第一題是隻進行一次交易,相當於 k = 1;第二題是不限交易次數,相當於 k = +infinity(正無窮);第三題是隻進行 2 次交易,相當於 k = 2;剩下兩道也是不限次數,但是加了交易「冷凍期」和「手續費」的額外條件,其實就是第二題的變種,都很容易處理。
一、窮舉框架
我們利用「狀態」進行窮舉。我們具體到每一天,看看總共有幾種可能的「狀態」,再找出每個「狀態」對應的「選擇」。我們要窮舉所有「狀態」,窮舉的目的是根據對應的「選擇」更新狀態。聽起來抽象,你只要記住「狀態」和「選擇」兩個詞就行,下面實操一下就很容易明白了。

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 種狀態,全部窮舉就能搞定。

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 次交易,最多獲得多少利潤。讀者可能問爲什麼不是 dp[n - 1][K][1]?因爲 [1] 代表手上還持有股票,[0] 表示手上的股票已經賣出去了,很顯然後者得到的利潤一定大於前者。

記住如何解釋「狀態」,一旦你覺得哪裏不好理解,把它翻譯成自然語言就容易理解了。

二、狀態轉移框架

現在,我們完成了「狀態」的窮舉,我們開始思考每種「狀態」有哪些「選擇」,應該如何更新「狀態」。只看「持有狀態」,可以畫個狀態轉移圖。

在這裏插入圖片描述

通過這個圖可以很清楚地看到,每種狀態(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 怎麼編程表示出來呢,負無窮怎麼表示呢?這都是細節問題,有很多方法實現。現在完整的框架已經完成,下面開始具體化。

三、秒殺題目
第一題,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])

我們之前的解法,都在窮舉所有狀態,只是之前的題目中 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 道題目通過一個狀態轉移方程全部解決。

四、最後總結

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

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

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

所以,大家不要被各種高大上的名詞嚇到,再多的困難問題,奇技淫巧,也不過是基本套路的不斷升級組合產生的。只要把住算法的底層原理,即可舉一反三,逐個擊破。

買賣股票的最佳時機

買賣股票的最佳時機 II

最佳買賣股票時機含冷凍期

買賣股票的最佳時機含手續費

買賣股票的最佳時機 III

買賣股票的最佳時機 IV

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章