題目來源:力扣
題目介紹:
給定一個整數數組 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.
審題:
對於該最優化問題, 我們考慮使用動態規劃算法解決. 在解決這道題時, 由於起初最優子結構問題設計不合理, 我的時間複雜度爲, 而後改進, 時間複雜度將爲, 然而提交仍然超時, 直到進一步更改最優子結構, 時間複雜度降爲, 才通過提交.
接下來, 我們從時間複雜度的算法開始, 逐一介紹我的思路.
在最初的設計中, 考慮從第k天開始的最優交易方案, 如果我們選擇第一筆交易爲第i天買入, 第j天賣出, 則如果我們能夠計算得到從第j+1天開始的最優方案, 則可以計算得到第k天的最優方案.當時這個思路我感覺很容易就想到了, 但後面也證實它是最低效的. 我們使用S[i]表示從第i天開始最優交易方案下的盈利, 可以得到如下狀態轉移方程:
在該問題中, 子問題規模爲, 每一步的選擇規模爲, 因此該問題時間複雜度爲.具體代碼實現如下:
class Solution {
public int maxProfit(int[] prices, int fee) {
if(prices.length == 1)
return 0;
int[] S = new int[prices.length+1];
//基礎情形
S[prices.length-1] = 0;
S[prices.length-2] = Math.max(0, prices[prices.length-1] - prices[prices.length-2] - fee);
for(int i = prices.length-3; i >= 0; i--){
//當前可以選擇的股票買入與賣出時間組合
int max = 0;
for(int buy = i; buy < prices.length-1; buy++){
for(int sell = buy+1; sell < prices.length; sell++){
max = Math.max(max, prices[sell]-prices[buy]- fee + S[sell+1]);
}
}
S[i] = max;
}
return S[0];
}
}
我們重新思考該問題, 對於每一天的股票, 可能包含兩種情形, 買入當日股票與不買入當日股票. 此時我們引入兩個狀態變量, 分別爲日期與是否買入當日股票.爲了計算從日期i開始,買入日期i股票條件下最大收益, 我們需要分別計算在日期往後的各個日期內賣出該股票的最大收益.
基於該最優子結構, 我們的子問題規模爲, 每一子問題的選擇規模爲. 因此算法的時間複雜度爲.具體代碼實現如下:
class Solution {
public int maxProfit(int[] prices, int fee) {
//狀態, 天數, 是否購買該日股票
int[][] S = new int[prices.length][2];
S[prices.length-1][0] = -prices[prices.length-1];
S[prices.length-1][1] = 0;
for(int i = prices.length-2; i >= 0; i--){
int bestBuy = Integer.MIN_VALUE;
//如果我在第i日購入了股票,
//則可以在第j日繼續持有,
//或這在第j日賣掉, 賣掉後可以買入第j日的, 也可以不買
for(int j = i+1; j < prices.length; j++){
//如果我在當日賣出了股票, 則我可以選擇買入當日股票, 或不買入
int sell = prices[j] - prices[i] - fee + Math.max(S[j][0], S[j][1]);
int donotsell = -prices[i] + S[j][1]; //如果我沒賣, 則不能買當日股票
bestBuy = Math.max(bestBuy, Math.max(sell, donotsell));
}
//如果我沒有購入第i日的股票
//則在第j日, 可以購入股票, 也可以不購入股票
int bestNotBuy = Integer.MIN_VALUE;
for(int j = i+1; j < prices.length; j++){
bestNotBuy = Math.max(bestNotBuy, Math.max(S[j][0], S[j][1]));
}
S[i][0] = bestBuy;
S[i][1] = bestNotBuy;
}
return Math.max(S[0][0], S[0][1]);
}
}
還存在其他最優子結構設計嗎? 我一開始是沒想到其他更好的方法, 後來看了他人的題解, 才發現真的妙.
我們仍然選擇兩個狀態變量, 一個爲日期, 一個表示當前用戶是否持有股票. 如果我們需要計期計算日期i時用戶持有股票所能獲得的最大收益, 我們需要計算在日期i-1時用戶持有股票的情形下, 在日期i不售出的收益, 以及在日期i不持有股票的情形下, 在日期i購入股票的收益. 兩者的最大值, 即爲用戶在日期i持有股票的最大收益.類似地, 爲了計算用戶在日期i不持有股票的收益, 我們需要計算用戶在日期i-1不持有股票並且不購買日期i股票的收益以及用戶在日期i-1持有股票但在日期i賣出的收益.
此時子問題規模爲, 每一子問題的選擇規模爲, 因此該算法的時間複雜度爲
class Solution {
public int maxProfit(int[] prices, int fee) {
//狀態, 天數, 是否持有當日股票
//每天的選擇, 持有, 賣出, 繼續保持
int[][] S = new int[prices.length][2];
S[0][0] = -prices[0];
S[0][1] = 0;
for(int i = 1; i < prices.length; i++){
//第i天持有的最佳方案即是前一天持有繼續保持, 或前一天不持有, 今日購買
S[i][0] = Math.max(S[i-1][0], S[i-1][1] - prices[i]);
S[i][1] = Math.max(S[i-1][1], S[i-1][0] + prices[i] - fee);
}
return S[prices.length-1][1];
}
}
用戶的最大收益即是在最後一天不持有股票的最大收益, 因爲在最後一天買入股票總是會使得總收益降低.
在當前的設計中, 算法的空間複雜度爲, 但由於當前狀態下的最優取值僅依賴於其前一狀態的最優取值, 因此我們可以進行狀態壓縮, 將空間複雜度降至.
class Solution {
public int maxProfit(int[] prices, int fee) {
//狀態, 天數, 是否持有當日股票
//每天的選擇, 持有, 賣出, 繼續保持
int hold = -prices[0];
int donotHold = 0;
for(int i = 1; i < prices.length; i++){
//第i天持有的最佳方案即是前一天持有繼續保持, 或前一天不持有, 今日購買
//如果用戶在當前買入, 在其在當天賣出的收益肯定小於其在當前不賣出的收益
//因此, 如果hold = donotHold-prices[i]
//則dontotHold = donotHold
//因此我們可以不使用中間變量保存hold未改變時的取值
hold = Math.max(hold, donotHold - prices[i]);
donotHold = Math.max(donotHold, hold + prices[i] - fee);
}
return donotHold;
}
}