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

題目來源:力扣

題目介紹:

給定一個整數數組 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.

審題:

對於該最優化問題, 我們考慮使用動態規劃算法解決. 在解決這道題時, 由於起初最優子結構問題設計不合理, 我的時間複雜度爲O(N3)O(N^3), 而後改進, 時間複雜度將爲O(N2)O(N^2), 然而提交仍然超時, 直到進一步更改最優子結構, 時間複雜度降爲O(N)O(N), 才通過提交.

接下來, 我們從時間複雜度O(N3)O(N^3)的算法開始, 逐一介紹我的思路.

在最初的設計中, 考慮從第k天開始的最優交易方案, 如果我們選擇第一筆交易爲第i天買入, 第j天賣出, 則如果我們能夠計算得到從第j+1天開始的最優方案, 則可以計算得到第k天的最優方案.當時這個思路我感覺很容易就想到了, 但後面也證實它是最低效的. 我們使用S[i]表示從第i天開始最優交易方案下的盈利, 可以得到如下狀態轉移方程:
S[i]=max{piles[t]piles[s]fee+S[t+1],si<t<prices.length}S[i] = max\{piles[t]-piles[s] - fee + S[t+1], s \leq i < t < prices.length\}

在該問題中, 子問題規模爲O(N)O(N), 每一步的選擇規模爲O(N2)O(N^2), 因此該問題時間複雜度爲O(N3)O(N^3).具體代碼實現如下:

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股票條件下最大收益, 我們需要分別計算在日期往後的各個日期內賣出該股票的最大收益.

基於該最優子結構, 我們的子問題規模爲O(N)O(N), 每一子問題的選擇規模爲O(N)O(N). 因此算法的時間複雜度爲O(N2)O(N^2).具體代碼實現如下:

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賣出的收益.

此時子問題規模爲O(N)O(N), 每一子問題的選擇規模爲O(1)O(1), 因此該算法的時間複雜度爲O(N)O(N)

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];
    }
}

用戶的最大收益即是在最後一天不持有股票的最大收益, 因爲在最後一天買入股票總是會使得總收益降低.

在當前的設計中, 算法的空間複雜度爲O(N)O(N), 但由於當前狀態下的最優取值僅依賴於其前一狀態的最優取值, 因此我們可以進行狀態壓縮, 將空間複雜度降至O(1)O(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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章