题目来源:力扣
题目介绍:
给定一个整数数组 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],s≤i<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;
}
}