【LeetCode動態規劃#13】買賣股票含冷凍期(狀態眾多,比較繁瑣)、含手續費

2023-04-26 06:00:45

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

力扣題目連結(opens new window)

給定一個整數陣列,其中第 i 個元素代表了第 i 天的股票價格 。

設計一個演演算法計算出最大利潤。在滿足以下約束條件下,你可以儘可能地完成更多的交易(多次買賣一支股票):

  • 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
  • 賣出股票後,你無法在第二天買入股票 (即冷凍期為 1 天)。

範例:

  • 輸入: [1,2,3,0,2]
  • 輸出: 3
  • 解釋: 對應的交易狀態為: [買入, 賣出, 冷凍期, 買入, 賣出]

思路

本題在買賣II的基礎上多了一個條件:即賣出股票後的一天不能交易(冷凍期)

我們在分析買賣II時有:持有不持有 兩種狀態,並且這兩種狀態中又細分為 當天買入/賣出 和 前一天買入/賣出

因為本題加了冷凍期,所以狀態會有不同,大體上可以分為:持有股票不持有股票處於冷凍期三大類

五步走

1、確定dp陣列含義

持有股票可以視為狀態1(今天買入或者前一天買入,因為引入了冷凍期,其中還有細分情況後面說)

基於此,我覺得買賣II的狀態也是可以縮減的(但這麼做可能就不夠直觀了)

因為有冷凍期限制賣出的操作,所以不持有股票時還是要細分為兩種情況

保持之前不持有股票的狀態視為狀態2

今天賣出股票視為狀態3

最後,也是新增的一個狀態,即賣出股票後的冷凍期,視為狀態4

綜上,本題一共有以下四種狀態:

狀態1---持有股票(j = 0)

不持有股票
	狀態2---保持不持有狀態(j = 1)
	狀態3---今天賣掉股票(j = 2)
狀態4---冷凍期(j = 3)

買賣II一樣,我們仍需要使用二維dp陣列dp[i][j]來表示狀態:代表第i天某種狀態下得到的最大收益

2、確定遞推公式
(1)持有(買入)股票dp[i][0](狀態1)

要達到dp[i][0],可以有兩個途徑:今天買入股票前一天買入股票

  • 今天買入股票又分為三種情況

    • (買入)前一天是冷凍期(狀態4,j = 3),即冷凍期一結束馬上買入,dp[i][0] = dp[i - 1][3] - prices[i]
    • (買入)前一天是不持有股票狀態(狀態2,j = 1),即昨天之前就已經賣掉股票並過了冷凍期,但是還沒有買入股票,今天買入dp[i][0] = dp[i - 1][1] - prices[i]

    兩者取最大值max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);

    • (維持狀態即前一天買入股票)前一天是保持著上一天買入的狀態(狀態1,j = 0),並持續到今天,dp[i][0] = dp[i - 1][0]

綜上,持有股票狀態(狀態1)的遞推公式是:dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));

(2)不持有股票,保持不持有狀態dp[i][1](狀態2)
  • 前一天為冷凍期(狀態4,j = 3),即兩天之前賣掉了股票,經過一天冷凍期到今天就是不持有狀態(不用再加prices),dp[i][1] = dp[i - 1][3]
  • 前一天就是不持有股票的狀態(狀態2,j = 1),即在昨天之前就已經賣掉股票並度過了冷凍期dp[i][1] = dp[i - 1][1]

綜上,保持不持有狀態(狀態2)的遞推公式是:dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

(3)今天賣掉股票dp[i][2](狀態3)
  • 前一天為冷凍期(狀態4,j = 3),即冷凍期剛結束馬上又賣出dp[i][2] = dp[i - 1][3] + prices[i]
  • 前一天為持有股票狀態(狀態1,j = 0),dp[i][2] = dp[i - 1][0] + prices[i]

綜上,今天賣掉股票(狀態3)的遞推公式是:dp[i][2] = max(dp[i - 1][3] + prices[i], dp[i - 1][0] + prices[i]);

綜上,今天賣掉股票(狀態3)的遞推公式是:dp[i][2] = dp[i - 1][0] + prices[i];

處於冷凍期就表明之前的一天一定賣出了股票,因此賣掉股票(即狀態3)的前置狀態不可能是冷凍期(狀態4),剛賣完股票還沒買呢沒東西可賣

(4)冷凍期dp[i][3]

冷凍期的前置狀態只可能是前一天賣出股票(狀態3),dp[i][3] = dp[i - 1][2]

總結一下上面三大類,四種狀態

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

這就是全部狀態的遞推公式

3、初始化dp陣列

有以下幾種情況需要進行初始化

(1)第0天買入股票(狀態1)(一定是買入而不是持有,因為第0天不會有前一天的狀態延續)

這種情況下肯定初始金錢為0,那麼dp[0][0] = -prices[0]

(2)第0天保持不持有狀態(狀態2)

要找的是dp[0][1]的情況,要在i等於1時討論,即dp[1][1] = max(dp[0][1], dp[0][3]);

此時dp[0][1]要取0

dp[1][1]指的是第1天保持不持有狀態,要在上述二者中取一個最大值

理解1:

那肯定是要使dp[0][3]更大一些才對啊,因為這是冷凍期狀態,意味著已經賣過一次股票,手裡肯定有錢

故為了不影響dp[0][3]dp[0][1]要取0

理解2:

第0天保持不持有狀態 和 第0天冷凍期狀態 都是 第1天保持不持有狀態 的前置狀態,既然最終目的都是要讓第1天保持不持有狀態,那麼其實dp[0][3]dp[0][1]都可以取0,這也解釋了dp[0][3]應該初始化為0的原因

(3)第0天賣出股票(狀態3)

要找的是dp[0][2]的情況,也要在i等於1時討論,結果與上面討論狀態2第0天初始化是同理的,dp[0][2]也應該取0

此外,dp[0][3]也初始化為0,見理解2↑

4、確定遍歷順序

從遞迴公式上可以看出,dp[i] 依賴於 dp[i-1],所以是從前向後遍歷。

程式碼

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0) return 0;
        //定義dp陣列
        vector<vector<int>> dp(prices.size(), vector<int>(4, 0));

        //初始化
        dp[0][0] = -prices[0];

        //遍歷dp陣列
        for(int i = 1; i < prices.size(); ++i){
            //買入股票:
            //維持前一天買入股票的狀態
            //前一天是冷凍期(狀態4,j = 3),冷凍期一結束馬上買入
            //前一天是不持有股票狀態(狀態2,j = 1),昨天之前就已經賣掉股票並過了冷凍期,但是還沒有買入股票,今天買入
            dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
            //不持有股票
            //前一天為冷凍期(狀態4,j = 3),兩天之前賣掉了股票,經過一天冷凍期到今天
            //前一天就是不持有股票的狀態,昨天之前就已經賣掉股票並度過了冷凍期
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];//前一天為持有股票狀態(狀態1,j = 0)
            dp[i][3] = dp[i - 1][2];//前一天賣出股票(狀態3)
        }
        //狀態2、3、4都有可能是最大值
        return max(dp[prices.size() - 1][3], max(dp[prices.size() - 1][1], dp[prices.size() - 1][2]));
    }
};

狀態2---保持不持有狀態(已經賣掉股票)

狀態3---今天賣掉股票

狀態4---冷凍期(賣掉股票才可能進入冷凍期)

為什麼返回的是狀態狀態2、3、4中的最大值,因為這三個狀態都賣出了股票,最後賣出股票才有可能得到最大收益

至於為什麼狀態2、3單獨取max?其實無所謂順序,只是max一次只能輸入兩個比較值所以要這樣寫

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

力扣題目連結(opens new window)

給定一個整數陣列 prices,其中第 i 個元素代表了第 i 天的股票價格 ;非負整數 fee 代表了交易股票的手續費用。

你可以無限次地完成交易,但是你每筆交易都需要付手續費。如果你已經購買了一個股票,在賣出它之前你就不能再繼續購買股票了。

返回獲得利潤的最大值。

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

範例 1:

  • 輸入: prices = [1, 3, 2, 8, 4, 9], fee = 2
  • 輸出: 8

解釋: 能夠達到的最大利潤:

  • 在此處買入 prices[0] = 1
  • 在此處賣出 prices[3] = 8
  • 在此處買入 prices[4] = 4
  • 在此處賣出 prices[5] = 9
  • 總利潤: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

注意:

  • 0 < prices.length <= 50000.
  • 0 < prices[i] < 50000.
  • 0 <= fee < 50000

思路

與買賣II幾乎一樣,只是需要加入手續費的處理邏輯即可

再推導一下所有的情況吧

(1)持有股票dp[i][0]

如果是第i天買入的,那麼要用沒有持有該股票時有的錢減去股票的售價,即dp[i][0] = dp[i - 1][1] - prices[i]

如果是第i-1天買入的,就還是和上一題一樣,狀態延續到第i天即可,即dp[i][0] = dp[i - 1][0]

綜上,dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);//持有

(2)不持有股票dp[i][1]

如果是第i天賣掉的,那就要用持有該股票時有的錢加上賣股票得的錢然後還要減掉手續費,即dp[i][1] = dp[i - 1][0] + prices[i] - fee

如果是第i-1天賣掉的,延續第i天的狀態即可,即dp[i][1] = dp[i - 1][1]

綜上,dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);//不持有

程式碼

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();//獲取prices陣列長度(天數)
        vector<vector<int>> dp(n, vector<int>(2, 0));//建立dp陣列
        dp[0][0] -= prices[0]; //初始化
        //dp[0][1] = 0;
        
        for (int i = 1; i < n; 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] - fee);//不持有
        }
        return max(dp[n - 1][0], dp[n - 1][1]);//最後一天要求把股票賣掉,返回不持有股票的最大金錢數
    }
};