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 ( N 3 ) O(N^3) O(N3), 而后改进, 时间复杂度将为 O ( N 2 ) O(N^2) O(N2), 然而提交仍然超时, 直到进一步更改最优子结构, 时间复杂度降为 O ( N ) O(N) O(N), 才通过提交.

接下来, 我们从时间复杂度 O ( N 3 ) O(N^3) O(N3)的算法开始, 逐一介绍我的思路.

在最初的设计中, 考虑从第k天开始的最优交易方案, 如果我们选择第一笔交易为第i天买入, 第j天卖出, 则如果我们能够计算得到从第j+1天开始的最优方案, 则可以计算得到第k天的最优方案.当时这个思路我感觉很容易就想到了, 但后面也证实它是最低效的. 我们使用S[i]表示从第i天开始最优交易方案下的盈利, 可以得到如下状态转移方程:
S [ i ] = m a x { p i l e s [ t ] − p i l e s [ s ] − f e e + S [ t + 1 ] , s ≤ i < t < p r i c e s . l e n g t h } S[i] = max\{piles[t]-piles[s] - fee + S[t+1], s \leq i < t < prices.length\} S[i]=max{piles[t]piles[s]fee+S[t+1],si<t<prices.length}

在该问题中, 子问题规模为 O ( N ) O(N) O(N), 每一步的选择规模为 O ( N 2 ) O(N^2) O(N2), 因此该问题时间复杂度为 O ( N 3 ) O(N^3) O(N3).具体代码实现如下:

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(N) O(N). 因此算法的时间复杂度为 O ( N 2 ) O(N^2) O(N2).具体代码实现如下:

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(N), 每一子问题的选择规模为 O ( 1 ) O(1) O(1), 因此该算法的时间复杂度为 O ( N ) 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(N), 但由于当前状态下的最优取值仅依赖于其前一状态的最优取值, 因此我们可以进行状态压缩, 将空间复杂度降至 O ( 1 ) 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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值