買賣股票的最佳時機
給定一個陣列 prices ,它的第 i 個元素 prices[i] 表示一支給定股票第 i 天的價格。
你只能選擇 某一天 買入這隻股票,並選擇在 未來的某一個不同的日子 賣出該股票。設計一個演演算法來計算你所能獲取的最大利潤。
返回你可以從這筆交易中獲取的最大利潤。如果你不能獲取任何利潤,返回 0 。
示例 1:
輸入:[7,1,5,3,6,4]
輸出:5
解釋:在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。注意利潤不能是 7-1 = 6, 因為賣出價格需要大於買入價格;同時,你不能在買入前賣出股票。
示例 2:
輸入:prices = [7,6,4,3,1]
輸出:0
解釋:在這種情況下, 沒有交易完成, 所以最大利潤為 0。
思路
買賣股票除了確定買賣日期之外,還需要確定當天股票的狀態
股票狀態有兩種:持有和不持有
為什麼不用"買入"和"賣出"來作為狀態?
因為買入和賣出僅能代表兩種狀態,即買入和賣出
什麼意思呢?就比如第i天你賣出了股票,好那第i+1天如果你不賣股票,那應該是什麼狀態?
如果還用“賣出”狀態來表示,那麼就意味著第i+1天仍然要賣出股票,但實際上我們想表示的是這樣一種狀態:“第i天賣出股票後,如果第i+1天不賣,那麼第i天賣掉股票後的狀態應該持續到第i+1天”
因此,使用"買入"和"賣出"來作為狀態會漏掉一些狀態,還得單獨為那些情況設定對應的狀態進行表示
五步走
1、確定dp陣列含義
根據思路,我們需要的dp陣列應該是二維的
dp[i][0]
:代表第i天持有股票所得到的最大收益
dp[i][1]
:代表第i天不持有股票所得到的最大收益
這裡在解釋一下"持有"和"不持有"
持有指的是我現在手頭上有某隻股票,但不一定是今天買的,有可能是之前某一天買的,然後該狀態延續到了現在
不持有指的是手頭上已經沒有股票了,但不一定是今天賣的,有可能是之前賣掉了,然後該狀態持續到了現在(因為本題的股票只能買賣一次,因此該狀態會持續到最後一天)
2、確定遞推公式
因為dp的定義是"持有"和"不持有"兩種,因此這兩種情況需要分開討論
(1)持有股票
第i天持有股票(dp[i][0]
)的狀態可以透過兩種情況推導得到:
- 第i天買了股票
- 第i - 1天之前買了股票
因為本題只能買賣一次股票,因此不管在哪天買入股票,都是第一次買入
因為我們的初始現金是0,所以在買入股票之後,此時的最大收益是 -price[i],即dp[i][0] = -price[i]
。因為買股票花錢了嘛,此時的狀態變為持有股票
然後如果是第i天不賣入股票,但狀態仍是持有股票的話,那麼此時的狀態是dp[i][0] = dp[i - 1][0]
綜上,取兩者最大的情況,那麼持有股票時的遞推公式為:dp[i][0] = max(dp[i - 1][0], -price[i])
(2)不持有股票
第i天持有股票(dp[i][1]
)的狀態可以透過兩種情況推導得到:
- 第i天賣了股票
- 第i - 1天之前賣了股票
如果是在第i天賣了股票,那麼此時除了狀態需要轉換為持有股票(並且是i - 1天持有,因為第i天賣掉了就不持有了),還需要加上第i天的股價price[i]作為收益,即dp[i][1] = price[i] + dp[i - 1][0]
如果是在第i - 1天之前賣了股票,那麼此時狀態還是不持有股票,即dp[i][1] = dp[i - 1][1]
綜上,取兩者最大的情況,那麼不持有股票時的遞推公式為:dp[i][1] = max(dp[i - 1][1], price[i] + dp[i - 1][0])
3、初始化dp陣列
從兩種情況下的遞推公式來看,dp[0][1]
和dp[0][0]
是遞推的基礎,因此需要對其進行初始化
結合dp陣列的定義,dp[0][1]
是第0天不持有股票所得到的最大收益,那沒有股票肯定是0啊,而且我們初始資金也是0,即dp[0][1]=0
而dp[0][0]
則是第0天持有股票所得到的最大收益,因為是第0天,所以不會存在前一天的情況
因此持有的股票一定是第0天購買的,所以dp[0][0] = -price[i]
4、確定遍歷順序
dp[i][1]
由dp[0][1]
推導而來,因此需要順序遍歷
程式碼
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
if(len == 0) return 0;
//定義dp陣列
vector<vector<int>> dp(len, vector<int>(2));
//初始化
dp[0][0] = -prices[0];
dp[0][1] = 0;
//遍歷dp陣列
for(int i = 1; i < len ;++i){
dp[i][0] = max(dp[i - 1][0], -prices[i]);//持有股票
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);//不持有股票
}
return dp[len - 1][1];
}
};
為什麼返回值是
dp[len - 1][1]
而不是max(dp[len][0], dp[len][1])
?
如果認為max(dp[len][0], dp[len][1])
是返回值的話,存在兩個錯誤
(1)len - 1才是最後一天,不是len
(2)題目要求最大利潤,但沒有要求具體在哪個狀態達成
在這個題目中,只需要求最終的最大利潤,而不需要知道具體是在哪個狀態(持有股票或不持有股票)達到了最大利潤,因此直接返回dp[len - 1][1]
即可。這是因為題目中規定,最後一天必須賣出所有持有的股票,而不是可以選擇繼續持有。
在遍歷到最後一天後,dp[len-1][0]
表示最後一天持有股票的最大收益,而dp[len-1][1]
則表示最後一天不持有股票的最大收益。
我們只需要考慮最後一天的狀態,也就是持有股票和不持有股票的兩種情況,因為最後一天無法再進行任何交易了,只有賣出股票才能獲得收益。因此,我們只需要返回最後一天不持有股票的最大收益 dp[len-1][1]
即可。
買賣股票的最佳時機II
給定一個陣列,它的第 i 個元素是一支給定股票第 i 天的價格。
設計一個演演算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例 1:
輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4。隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。
示例 2:
輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。因為這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。
示例 3:
輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種情況下, 沒有交易完成, 所以最大利潤為 0。
提示:
- 1 <= prices.length <= 3 * 10 ^ 4
- 0 <= prices[i] <= 10 ^ 4
思路
基本的思路和 I 一致
唯一不同的地方,就是推導dp[i][0]
和dp[i][0]
的時候,第i天買入股票的情況。
這裡再推導一下所有的情況吧
(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]
如果是第i-1天賣掉的,延續第i天的狀態即可,即dp[i][1] = dp[i - 1][1]
綜上,dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);//不持有
程式碼
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();//獲取prices陣列長度(天數)
if(len == 0 ) return 0;
vector<vector<int>> dp(len, vector<int>(2));//建立dp陣列
dp[0][0] = -prices[0];//初始化
dp[0][1] = 0;
for(int i = 1; i < len; ++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[len - 1][1];//最後一天要求把股票賣掉,返回不持有股票的最大金錢數
}
};
買賣股票的最佳時機III
給定一個陣列,它的第 i 個元素是一支給定的股票在第 i 天的價格。
設計一個演演算法來計算你所能獲取的最大利潤。你最多可以完成 兩筆 交易。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例 1: 輸入:prices = [3,3,5,0,0,3,1,4] 輸出:6 解釋:在第 4 天(股票價格 = 0)的時候買入,在第 6 天(股票價格 = 3)的時候賣出,這筆交易所能獲得利潤 = 3-0 = 3 。隨後,在第 7 天(股票價格 = 1)的時候買入,在第 8 天 (股票價格 = 4)的時候賣出,這筆交易所能獲得利潤 = 4-1 = 3。
示例 2: 輸入:prices = [1,2,3,4,5] 輸出:4 解釋:在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4。注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。因為這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。
示例 3: 輸入:prices = [7,6,4,3,1] 輸出:0 解釋:在這個情況下, 沒有交易完成, 所以最大利潤為0。
示例 4: 輸入:prices = [1] 輸出:0
提示:
- 1 <= prices.length <= 10^5
- 0 <= prices[i] <= 10^5
思路
與之前的兩題最大的不同是,之前一天只可以進行一次交易(買賣),總的交易次數也只有1次
而現在一天可以交易兩次,總的次數也是兩次
至多買賣兩次,意味著可以買賣一次,可以買賣兩次,也可以不買賣。
五步走
1、確定dp陣列含義
回憶一下,之前只能買賣一次時,我們有兩種情況,即:第i天持有或不持有股票
現在因為可以進行兩次買賣,所以第i天可能有的所有情況(假設在第i天就進行兩次交易,就會有四種情況)有四種:
- 0、沒有操作--
dp[i][0]
- 1、第i天第一次持有股票--
dp[i][1]
- 2、第i天第一次不持有股票--
dp[i][2]
- 3、第i天第二次持有股票--
dp[i][3]
- 4、第i天第二次不持有股票--
dp[i][4]
即dp[i][j]
中,i表示第i天,j表示狀態
dp[i][j]
表示第i天狀態j所剩最大現金
2、確定遞推公式
dp[i][0]
先不用管,後面要初始化
達到dp[i][1]
狀態(第一次持有股票),有兩個具體操作:
- 操作一:第i天買入股票了,那麼
dp[i][1] = dp[i-1][0] - prices[i]
(用前一天狀態下還剩的錢減去第i天買入股票的價格) - 操作二:第i天沒有操作,而是沿用前一天買入的狀態,即:
dp[i][1] = dp[i - 1][1]
取最大結果:dp[i][1] = max(dp[i- 1][0] - prices[i], dp[i - 1][1]);
dp[i][2]
(第一次不持有股票)同理:
- 操作一:第i天賣出股票了,那麼
dp[i][2] = dp[i - 1][1] + prices[i]
( ) - 操作二:第i天沒有操作,沿用前一天賣出股票的狀態,即:
dp[i][2] = dp[i - 1][2]
取最大結果:dp[i][2] = max(dp[i - 2][1] - prices[i], dp[i - 1][2]);
按上面的形式可以把剩餘的情況在不同操作下的狀態推匯出來:
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
3、初始化dp陣列
(1)如果第0天沒有操作,那就是0,即dp[0][0] = 0
(2)如果第0天第一次操作
- 在第0天第一次買入,初始現金為0,那麼現在要扣除買股票的錢,即
dp[0][1] = -prices[0]
- 在第0天第一次賣出,即
dp[0][2] = 0
如果第0天的第一次操作就是賣出,那此時都沒有東西,賣什麼呢?
這種情況可以理解為在同一天先買入了,然後又在當天賣出
買賣都是相同的價格,相當於錢花出去了又原路返回,因此
dp[0][2] = 0
由此我們可以發現,第一次賣出的操作是依賴第一次買入操作的
(3)如果第0天第二次操作
- 在第0天第二次買入。
dp[0][3] = -prices[0]
所謂的“第二次買入”,意味著我之前已經買入過一次了,也就是說第二次買入依賴於第一次賣出的狀態
又因為題目規定買完必須先賣掉才能再買,所以“第二次買入”時,已經經過了第一次買入和賣出
因此在第0天第二次買入時,手頭上的錢仍然是0,那麼買入之後的狀態自然就是 -prices[0]
- 在第0天第二次賣出。
dp[0][4] = 0
與在第0天第一次賣出同理
4、確定遍歷順序
i由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>(5, 0));
//初始化,根據分析進行初始化
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
//遍歷dp陣列
for(int i = 1; i < prices.size(); ++i){
dp[i][0] = dp[i - 1][0];//不進行操作
//第i天第一次進行操作
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);//不操作|買入股票,用前一天狀態下還剩的錢減去第i天買入股票的價格
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);//不操作|賣出股票,用前一天狀態下還剩的錢加上第i天賣掉股票的收益
//第i天第二次進行操作
dp[i][3] = max(dp[i - 1][3],dp[i - 1][2] - prices[i]);//不操作|買入股票
dp[i][4] = max(dp[i - 1][4],dp[i - 1][3] + prices[i]);//不操作|賣出股票
}
return dp[prices.size() - 1][4];
}
};
買賣股票的最佳時機IV
給定一個整數陣列 prices ,它的第 i 個元素 prices[i] 是一支給定的股票在第 i 天的價格。
設計一個演演算法來計算你所能獲取的最大利潤。你最多可以完成 k 筆交易。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
示例 1: 輸入:k = 2, prices = [2,4,1] 輸出:2 解釋:在第 1 天 (股票價格 = 2) 的時候買入,在第 2 天 (股票價格 = 4) 的時候賣出,這筆交易所能獲得利潤 = 4-2 = 2。
示例 2: 輸入:k = 2, prices = [3,2,6,5,0,3] 輸出:7 解釋:在第 2 天 (股票價格 = 2) 的時候買入,在第 3 天 (股票價格 = 6) 的時候賣出, 這筆交易所能獲得利潤 = 6-2 = 4。隨後,在第 5 天 (股票價格 = 0) 的時候買入,在第 6 天 (股票價格 = 3) 的時候賣出, 這筆交易所能獲得利潤 = 3-0 = 3 。
提示:
- 0 <= k <= 100
- 0 <= prices.length <= 1000
- 0 <= prices[i] <= 1000
思路
本題與上題的不同點在於買賣次數,現在我們可以任意買賣k次了
從上題來看,買賣兩次已經需要列出4個狀態了(不算不操作狀態),因此在k次交易的條件下,羅列處所有狀態是可能的
需要使用迴圈去控制
五步走
1、dp陣列的含義
與上一題一樣
二維陣列 dp[i][j]
:dp[i][j]
表示第i天狀態j所剩最大現金
j的狀態可以表示為:
- 0 表示不操作
- 1 第一次買入
- 2 第一次賣出
- 3 第二次買入
- 4 第二次賣出
- .....
除了0以外,上述狀態的規律可以總結為:偶數賣出,奇數買入
因為買賣次數變成k次,每多一次買賣會新增兩種狀態,所以dp陣列的大小要設定為2k + 1
vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
2、確定遞推公式
正如上面的分析,k次買賣的話是不可能都列出來所有狀態的,因此需要進行抽象,抽象出一個通用的遞推公式
先來看看上一題中,兩次買賣時的遞推公式:
dp[i][0] = dp[i - 1][0];//不進行操作
//第i天第一次進行操作
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);//不操作|買入股票,用前一天狀態下還剩的錢減去第i天買入股票的價格
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);//不操作|賣出股票,用前一天狀態下還剩的錢加上第i天賣掉股票的收益
//第i天第二次進行操作
dp[i][3] = max(dp[i - 1][3],dp[i - 1][2] - prices[i]);//不操作|買入股票
dp[i][4] = max(dp[i - 1][4],dp[i - 1][3] + prices[i]);//不操作|賣出股票
這裡可以發現,二維dp陣列中,第二個維度我們是寫成具體數字的,用來表示買入和賣出狀態
可以使用一個變數j來代替具體的數字,j + 1表示買入;j + 2表示賣出
使用for迴圈控制j,j從0開始迴圈,每次自增2
for(int j = 0; i < 2 * k; j += 2){
dp[i][j] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);//不操作|奇數買入,用前一天狀態下還剩的錢減去第i天買入股票的價格
dp[i][j] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);//不操作|偶數賣出,用前一天狀態下還剩的錢加上第i天賣掉股票的收益
}
類比j為奇數是買,偶數是賣的狀態。
為什麼j是小於2 * k,而不是別的值,比如2 * k - 1 ?
遇到邊界不確定的情況時,就代入具體值來判斷是否合理
這裡假設一共至多可以買賣2次
如果j < 2 * k ,那麼 j < 4
以上面 兩次買賣時的遞推公式 為例來看
當迴圈開始,
j = 0,所以 j + 1 指向
dp[i][1]
, j + 2 指向dp[i][2]
,一切正常j = 2,所以 j + 1 指向
dp[i][3]
, j + 2 指向dp[i][4]
,一切正常j = 3,所以 j + 1 指向
dp[i][3]
, j + 2 指向dp[i][4]
,一切正常j = 4,迴圈結束,所有狀態被完整覆蓋,邊界正確
3、初始化dp陣列
基本上與買賣III的思路一致
(1)如果第0天沒有操作,那就是0,即dp[0][0] = 0
(2)如果第0天第一次操作
- 在第0天第一次買入,初始現金為0,那麼現在要扣除買股票的錢,即
dp[0][1] = -prices[0]
- 在第0天第一次賣出,即
dp[0][2] = 0
(3)如果第0天第二次操作
- 在第0天第二次買入。
dp[0][3] = -prices[0]
- 在第0天第二次賣出。
dp[0][4] = 0
(關於推導的細節與註釋詳見買賣III)
我們還是從上面初始化中總結規律進行抽象
即:j為奇數時候,要初始化為-prices[0];j為偶數時候,要初始化為0;
使用for迴圈進行初始化
for(int j = 1; j < 2 * k; j += 2){
dp[0][j] = -prices[0];//0在建立dp陣列的時候就初始化了
}
為什麼j是小於2 * k,而不是別的值,比如2 * k - 1 ?
還是代入來看, 這裡假設至多可以買賣2次,那麼 j < 4
當j = 1時,奇數初始化為-prices[0]
然後自增2,j = 3,奇數初始化為-prices[0]
再自增就超過4,結束迴圈,過程沒有問題
4、確定遍歷順序
i由i-1推出,因此仍是從小到大遍歷,即順序遍歷
程式碼
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if(prices.size() == 0) return 0;
//建立dp陣列
vector<vector<int>> dp(prices.size() + 1, vector<int>(2 * k + 1, 0));
//初始化dp陣列
for(int j = 1; j < 2 * k; j += 2){
dp[0][j] = -prices[0];//0在建立dp陣列的時候就初始化了,所以不用在此處初始化
}
//遍歷dp陣列
for(int i = 1; i < prices.size(); ++i){
for(int j = 0; j < 2 * k; j += 2){
//不操作|奇數買入,用前一天狀態下還剩的錢減去第i天買入股票的價格
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
//不操作|偶數賣出,用前一天狀態下還剩的錢加上第i天賣掉股票的收益
dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
}
return dp[prices.size() - 1][2 * k];//返回最後一天賣掉股票後的總金額
}
};