188. 买卖股票的最佳时机 IV
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 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 。
方法一:三维 dp数组
// 版本一: 三维 dp数组
class Solution {public int maxProfit(int k, int[] prices) {if (prices.length == 0) return 0;// [天数][交易次数][是否持有股票]int len = prices.length;int[][][] dp = new int[len][k + 1][2];// dp数组初始化// 初始化所有的交易次数是为确保 最后结果是最多 k 次买卖的最大利润for (int i = 0; i <= k; i++) {dp[0][i][1] = -prices[0];}for (int i = 1; i < len; i++) {for (int j = 1; j <= k; j++) {// dp方程, 0表示不持有/卖出, 1表示持有/买入dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);}}return dp[len - 1][k][0];}
}
这段代码展示了如何使用三维动态规划数组来解决一个更通用的问题:给定一个整数 k
和一个整数数组 prices
,找出在最多进行 k
次买卖交易的情况下可以获取的最大利润。这里的 prices
数组表示股票每天的价格,而 k
表示允许进行买卖交易的最大次数。
在三维数组 dp
中:
- 第一维表示时间(天数),范围是
0
到len - 1
(len
是价格数组的长度)。 - 第二维表示当前允许的交易次数,范围是
0
到k
。 - 第三维是一个二元组,其中
0
表示不持有股票(即处于卖出状态),1
表示持有股票(即处于买入状态)。
初始化时,dp[0][i][1]
被设置为 -prices[0]
对于所有 i
(表示在第一天买入股票的成本),而 dp[0][i][0]
被隐式初始化为 0
(因为还没有进行任何交易)。
动态规划的状态转移方程如下:
dp[i][j][0]
更新为前一天dp[i-1][j][0]
和前一天持有股票并今天卖出dp[i-1][j][1] + prices[i]
中的较大值。dp[i][j][1]
更新为前一天dp[i-1][j][1]
和前一天未持有股票(即前一天卖出或没有进行过任何交易)并且今天买入dp[i-1][j-1][0] - prices[i]
中的较大值。
最终,dp[len - 1][k][0]
将包含在给定价格序列下最多进行 k
次买卖交易所能获得的最大利润。
现在,让我们使用同样的股票价格数组 [7, 1, 5, 3, 6, 4]
并设定 k=2
(允许进行两次交易),来计算最大利润。
def max_profit_k_transactions(prices, k):len_prices = len(prices)# Initialize DP arraydp = [[[0 for _ in range(2)] for _ in range(k + 1)] for _ in range(len_prices)]# Initialize all transactions to ensure the last result is the maximum profit with at most k transactionsfor i in range(k + 1):dp[0][i][1] = -prices[0]# DP calculationfor i in range(1, len_prices):for j in range(1, k + 1):dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i])dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i])return dp[len_prices - 1][k][0]# Using the same example stock price array and setting k=2
prices = [7, 1, 5, 3, 6, 4]
k = 2
max_profit_value_k_transactions = max_profit_k_transactions(prices, k)
max_profit_value_k_transactions
调用结果
7
使用三维动态规划数组的方法,对于股票价格数组 [7, 1, 5, 3, 6, 4]
并且允许最多进行两次交易 (k=2
) 的情况下,计算出的最大利润同样是 7
。
这表明算法正确地找到了在给定价格序列和交易次数限制下可以获得的最大利润。这种方法虽然空间复杂度较高(O(n*k)),但在 k
较小的情况下,它能有效地解决问题,并且提供了处理任意次数交易的能力。
如果您有任何疑问,或者想要了解其他相关问题,请随时告诉我!
方法二:二维dp数组
// 版本二: 二维 dp数组
class Solution {public int maxProfit(int k, int[] prices) {if (prices.length == 0) return 0;// [天数][股票状态]// 股票状态: 奇数表示第 k 次交易持有/买入, 偶数表示第 k 次交易不持有/卖出, 0 表示没有操作int len = prices.length;int[][] dp = new int[len][k*2 + 1];// dp数组的初始化, 与版本一同理for (int i = 1; i < k*2; i += 2) {dp[0][i] = -prices[0];}for (int i = 1; i < len; i++) {for (int j = 0; j < k*2 - 1; j += 2) {dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);}}return dp[len - 1][k*2];}
}
这个版本的代码通过使用二维动态规划数组来优化空间复杂度,同时仍然能够计算最多进行 k
次买卖交易的最大利润。与之前的三维数组版本相比,这个版本的数组只包含两个维度:天数和股票状态。
在这个二维数组 dp
中:
- 第一维表示时间(天数),范围是
0
到len - 1
(len
是价格数组的长度)。 - 第二维表示股票状态,其中奇数索引(从
1
开始)表示持有股票(买入状态),偶数索引(从0
开始)表示不持有股票(卖出状态)。这里利用了k * 2 + 1
的大小来容纳所有可能的交易状态,包括不进行任何交易的情况。
初始化时,所有奇数索引的 dp[0][i]
被设置为 -prices[0]
,这代表在第一天买入股票的成本。
动态规划的状态转移方程简化为:
dp[i][j + 1]
更新为前一天持有股票状态dp[i-1][j + 1]
和前一天不持有股票状态并今天买入dp[i-1][j] - prices[i]
中的较大值。dp[i][j + 2]
更新为前一天不持有股票状态dp[i-1][j + 2]
和前一天持有股票状态并今天卖出dp[i-1][j + 1] + prices[i]
中的较大值。
由于数组的第二维表示的是状态,所以不需要再单独跟踪交易次数;状态自动编码了交易次数的信息。最后一天的最后一个状态(dp[len - 1][k*2]
)将包含在给定价格序列下最多进行 k
次买卖交易所能获得的最大利润。
现在,我们使用相同的股票价格数组 [7, 1, 5, 3, 6, 4]
和 k=2
来计算最大利润。
def max_profit_k_transactions_2d(prices, k):len_prices = len(prices)# Initialize DP arraydp = [[0 for _ in range(k * 2 + 1)] for _ in range(len_prices)]# Initialize buy states to ensure the last result is the maximum profit with at most k transactionsfor i in range(1, k * 2, 2):dp[0][i] = -prices[0]# DP calculationfor i in range(1, len_prices):for j in range(0, k * 2 - 1, 2):dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i])dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i])return dp[len_prices - 1][k * 2]# Using the same example stock price array and setting k=2
prices = [7, 1, 5, 3, 6, 4]
k = 2
max_profit_value_k_transactions_2d = max_profit_k_transactions_2d(prices, k)
max_profit_value_k_transactions_2d
调用结果
7
使用二维动态规划数组的方法,对于相同的股票价格数组 [7, 1, 5, 3, 6, 4]
和允许最多进行两次交易 (k=2
) 的情况下,计算出的最大利润同样为 7
。
这表明即使在减少了一个维度的情况下,算法依然能够正确找到在给定价格序列和交易次数限制下的最大利润。这种方法的空间复杂度降低至 O(n),其中 n 是价格数组的长度,但时间复杂度保持不变,依然是 O(n*k)。这种优化对于处理大规模数据集尤其有用,因为它减少了内存使用量,同时也可能提高运行速度,特别是在 k
的值相对较大的时候。
如果您对这个方法有任何疑问,或者想进一步讨论相关问题,欢迎继续提问!
方法三:一维dp数组
//版本三:一维 dp数组 (下面有和卡哥邏輯一致的一維數組JAVA解法)
class Solution {public int maxProfit(int k, int[] prices) {if(prices.length == 0){return 0;}if(k == 0){return 0;}// 其实就是123题的扩展,123题只用记录2次交易的状态// 这里记录k次交易的状态就行了// 每次交易都有买入,卖出两个状态,所以要乘 2int[] dp = new int[2 * k];// 按123题解题格式那样,做一个初始化for(int i = 0; i < dp.length / 2; i++){dp[i * 2] = -prices[0];}for(int i = 1; i <= prices.length; i++){dp[0] = Math.max(dp[0], -prices[i - 1]);dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]);// 还是与123题一样,与123题对照来看// 就很容易啦for(int j = 2; j < dp.length; j += 2){dp[j] = Math.max(dp[j], dp[j - 1] - prices[i-1]);dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i - 1]);}}// 返回最后一次交易卖出状态的结果就行了return dp[dp.length - 1];}
}
这个版本的代码进一步优化了空间复杂度,使用了一维动态规划数组来存储中间结果。与之前二维数组版本相比,它不再需要一个完整的列来保存每一天的状态,而是直接在同一个数组上更新状态。这种优化使得空间复杂度降低到 O(k),其中 k 是允许的最大交易次数。
在这个一维数组 dp
中:
- 数组的大小为
2 * k
,其中奇数位置表示持有股票(买入状态),偶数位置表示不持有股票(卖出状态)。 - 初始化时,所有偶数位置被设置为
-prices[0]
,即第一次买入股票的成本。 - 动态规划的状态转移方程与二维数组版本相似,但是因为数组是一维的,所以需要在循环中交替更新状态。
关键点在于如何在不保留历史数据的情况下正确地更新 dp
数组。为了确保状态的正确性,在每次更新前,先更新买入状态,再更新卖出状态,这样可以避免未来状态影响过去状态的问题。
现在,让我们使用相同的价格数组 [7, 1, 5, 3, 6, 4]
和 k=2
来验证这个一维数组方法是否也能得到正确答案。
def max_profit_k_transactions_1d(prices, k):if not prices or k == 0:return 0dp = [0] * (2 * k)for i in range(0, k):dp[i * 2] = -prices[0]for i in range(1, len(prices) + 1):dp[0] = max(dp[0], -prices[i - 1])dp[1] = max(dp[1], dp[0] + prices[i - 1])for j in range(2, 2 * k, 2):dp[j] = max(dp[j], dp[j - 1] - prices[i - 1])dp[j + 1] = max(dp[j + 1], dp[j] + prices[i - 1])return dp[-1]# Using the same example stock price array and setting k=2
prices = [7, 1, 5, 3, 6, 4]
k = 2
max_profit_value_k_transactions_1d = max_profit_k_transactions_1d(prices, k)
max_profit_value_k_transactions_1d
调用结果
7
使用一维动态规划数组的方法,对于相同的股票价格数组 [7, 1, 5, 3, 6, 4]
和允许最多进行两次交易 (k=2
) 的情况下,计算出的最大利润同样为 7
。
这证明了一维数组版本的代码同样能够正确地解决问题,并且在空间效率方面比之前的版本更优。这种方法特别适用于当 k
较大时,因为其空间复杂度仅取决于 k
而非 n
,从而减少了大量内存的使用。
如果您对这个一维数组版本的算法有任何疑问,或想要了解关于动态规划的更多细节,随时欢迎提问!
方法四:
class Solution {public int maxProfit(int k, int[] prices) {//edge casesif(prices.length == 0 || k == 0)return 0;int dp[] = new int [k * 2 + 1];//和卡哥邏輯一致,奇數天購入股票,故初始化只初始化奇數天。for(int i = 1; i < 2 * k + 1; i += 2){dp[i] = -prices[0];}for(int i = 1; i < prices.length; i++){ //i 從 1 開始,因爲第 i = 0 天已經透過初始化完成了。for(int j = 1; j < 2 * k + 1; j++){ //j 從 1 開始,因爲第 j = 0 天已經透過初始化完成了。//奇數天購買if(j % 2 == 1)dp[j] = Math.max(dp[j], dp[j - 1] - prices[i]);//偶數天賣出elsedp[j] = Math.max(dp[j], dp[j - 1] + prices[i]);}//打印DP數組//for(int x : dp)// System.out.print(x +", ");//System.out.println();}//return 第2 * k次賣出的獲利。return dp[2 * k];}
}
这段代码实现了一个基于一维动态规划数组的解决方案,用于计算给定一系列股票价格和一个整数 k
(最大交易次数)情况下的最大可能利润。以下是代码的详细解释:
-
Edge Cases:
- 如果价格数组为空或者
k
(最大交易次数)为0,则直接返回0,因为没有交易发生。
- 如果价格数组为空或者
-
初始化动态规划数组:
- 创建一个长度为
k * 2 + 1
的数组dp
。这里+ 1
是为了确保数组包含k
次交易后的最终状态。 - 因为数组中的奇数位置代表持有股票的状态(买入),所以这些位置初始化为
-prices[0]
。这是假设在第一天购买股票的亏损(负利润),因为还没有卖出。
- 创建一个长度为
-
动态规划迭代:
- 从第二天开始(
i = 1
),遍历价格数组。 - 对于每一个价格
prices[i]
,遍历动态规划数组dp
(从j = 1
开始)。- 当
j
是奇数时,表示我们处于买入状态。此时,我们计算如果今天买入股票所能获得的最大利润(或者最小亏损)。因此,我们比较dp[j - 1] - prices[i]
和当前dp[j]
的值,选择较大者。 - 当
j
是偶数时,表示我们处于卖出状态。此时,我们计算如果今天卖出股票所能获得的最大利润。因此,我们比较dp[j - 1] + prices[i]
和当前dp[j]
的值,选择较大者。
- 当
- 从第二天开始(
-
返回值:
- 最终,
dp[2 * k]
表示在进行了k
次交易后(即进行了k
次买入和k
次卖出),所获得的最大利润。
- 最终,
注意到代码中有一个小错误,它在更新 dp[j]
的时候没有考虑到 j
可能超出 2 * k
的范围,但是由于 dp
数组的长度设置为 k * 2 + 1
,所以实际上这个错误不会导致运行时错误,但理论上来说,dp
的有效索引范围应该是 [0, 2 * k]
。
此外,迭代过程中 dp[j - 1]
的使用是合理的,因为它表示上一个状态,而上一个状态可能是买入也可能是卖出,这取决于 j
的奇偶性。
最后,代码中注释掉的部分是用来打印每次迭代后 dp
数组的值,这对于调试和理解动态规划的过程非常有用。
309. 买卖股票的最佳时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
方法一:
class Solution {public int maxProfit(int[] prices) {if (prices == null || prices.length < 2) {return 0;}int[][] dp = new int[prices.length][2];// bad casedp[0][0] = 0;dp[0][1] = -prices[0];dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]);dp[1][1] = Math.max(dp[0][1], -prices[1]);for (int i = 2; i < prices.length; i++) {// dp公式dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]);}return dp[prices.length - 1][0];}
}
这段代码是一个Java实现,用于解决股票买卖问题中的一个变种:允许进行多次交易,但是每次交易之后必须休息一天才能进行下一次交易。这个问题通过动态规划的方法解决。下面是代码的详细解释:
-
初始化:
- 首先检查输入数组是否为null或长度小于2,如果是,则直接返回0,因为没有足够的数据进行交易。
-
创建动态规划数组:
- 创建一个二维数组
dp
,其中dp[i][0]
表示在第i
天结束时不持有股票所能获得的最大利润,dp[i][1]
表示在第i
天结束时持有股票所能获得的最大利润。
- 创建一个二维数组
-
处理边界情况:
- 第0天,不持有股票的最大利润是0,持有股票的最大利润是买入股票的成本(用负数表示亏损)。
- 第1天,更新
dp[1][0]
和dp[1][1]
的值。dp[1][0]
表示在第1天结束时不持有股票的最大利润,它可以是从第0天开始就不持有股票 (dp[0][0]
) 或者第0天持有股票并在第1天卖出 (dp[0][1] + prices[1]
) 中取较大的一个;dp[1][1]
表示在第1天结束时持有股票的最大利润,它可以是第0天就持有股票 (dp[0][1]
) 或者在第1天买入股票 (-prices[1]
) 中取较大的一个。
-
动态规划迭代:
- 从第2天开始,对于每一天,我们更新
dp[i][0]
和dp[i][1]
的值。dp[i][0]
更新逻辑:在第i
天结束时不持有股票的最大利润,可以从第i-1
天就不持有股票 (dp[i-1][0]
) 或者第i-1
天持有股票并在第i
天卖出 (dp[i-1][1] + prices[i]
) 中取较大的一个。dp[i][1]
更新逻辑:在第i
天结束时持有股票的最大利润,可以从第i-1
天就持有股票 (dp[i-1][1]
) 或者在第i
天买入股票,但因为规则限制,需要考虑在第i-2
天结束时不持有股票 (dp[i-2][0] - prices[i]
) 中取较大的一个。
- 从第2天开始,对于每一天,我们更新
-
返回结果:
- 返回
dp[prices.length - 1][0]
,即在最后一天结束时不持有股票所能获得的最大利润。
- 返回
注意,这里的 dp[i-2][0]
在更新 dp[i][1]
的时候使用,是因为题目规定了每次交易之后必须休息一天,所以不能连续两天都进行买卖操作。这样,dp[i][1]
的值要么继承自前一天的 dp[i-1][1]
(保持持有股票),要么来自前前天的 dp[i-2][0]
(卖出了股票并休息了一天后重新买入)。
方法二:
//using 2*4 array for space optimization
//這裡稍微說一下,我在LeetCode提交的時候,2*4 2-D array的performance基本上和下面的1-D array performance差不多
//都是time: 1ms, space: 40.X MB (其實 length*4 的 2-D array也僅僅是space:41.X MB,看起來不多)
//股票累的DP題目大致上都是這樣,就當作是一個延伸就好了。真的有人問如何優化,最起碼有東西可以講。
class Solution {/**1. [i][0] holding the stock2. [i][1] after cooldown but stil not buing the stock3. [i][2] selling the stock4. [i][3] cooldown*/public int maxProfit(int[] prices) {int len = prices.length;int dp[][] = new int [2][4];dp[0][0] = -prices[0];for(int i = 1; i < len; i++){dp[i % 2][0] = Math.max(Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]), dp[(i - 1) % 2][3] - prices[i]);dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][3]);dp[i % 2][2] = dp[(i - 1) % 2][0] + prices[i];dp[i % 2][3] = dp[(i - 1) % 2][2];}return Math.max(Math.max(dp[(len - 1) % 2][1], dp[(len - 1) % 2][2]), dp[(len - 1) % 2][3]);}
}
这段代码提供了一种优化的空间复杂度解法,用于解决股票买卖问题中的一个变体,其中在卖出股票后需要等待一天的冷却期才能再次买入。这种变体要求我们找到能够最大化利润的交易策略。
解析
状态定义
dp[i % 2][0]
: 表示在第i
天结束时,手中持有股票的最大利润。dp[i % 2][1]
: 表示在第i
天结束时,处于冷却期但不持有股票的最大利润。dp[i % 2][2]
: 表示在第i
天结束时,刚刚卖出股票(不再处于冷却期)的最大利润。dp[i % 2][3]
: 表示在第i
天结束时,处于冷却期的最大利润。
边界条件
- 初始化
dp[0][0]
为-prices[0]
,表示在第0天买入股票的初始状态。
动态转移方程
dp[i % 2][0]
: 要么保持昨天持有股票的状态dp[(i - 1) % 2][0]
,要么昨天未持有股票(冷却期或未买入状态)并今天买入股票dp[(i - 1) % 2][1] - prices[i]
或dp[(i - 1) % 2][3] - prices[i]
。dp[i % 2][1]
: 保持昨天冷却期的状态dp[(i - 1) % 2][1]
或者从昨天卖出股票进入冷却期dp[(i - 1) % 2][3]
。dp[i % 2][2]
: 由昨天持有股票的状态卖出股票得到dp[(i - 1) % 2][0] + prices[i]
。dp[i % 2][3]
: 直接由昨天卖出股票的状态继承dp[(i - 1) % 2][2]
。
结果返回
- 返回最后一天所有四种状态中的最大值,即
Math.max(Math.max(dp[(len - 1) % 2][1], dp[(len - 1) % 2][2]), dp[(len - 1) % 2][3])
。
为什么使用i % 2
使用 i % 2
的目的是为了空间优化。由于动态规划只依赖于前两个状态,我们可以将 dp
数组设计为只有两行,交替使用这两行存储当前和前一时刻的状态,从而将空间复杂度降低到 O(1)
(不考虑输出结果的空间消耗)。
这种方法称为“空间优化”,通常在动态规划问题中用来减少所需的额外空间。在本例中,它使得原本需要 O(n)
空间的算法仅使用常数级别的额外空间。
方法三:
// 一维数组优化
class Solution {public int maxProfit(int[] prices) {int[] dp=new int[4];dp[0] = -prices[0];dp[1] = 0;for(int i = 1; i <= prices.length; i++){// 使用临时变量来保存dp[0], dp[2]// 因为马上dp[0]和dp[2]的数据都会变 int temp = dp[0];int temp1 = dp[2];dp[0] = Math.max(dp[0], Math.max(dp[3], dp[1]) - prices[i]);dp[1] = Math.max(dp[1], dp[3]);dp[2] = temp + prices[i];dp[3] = temp1;}return Math.max(dp[3],Math.max(dp[1],dp[2]));}
}
这段代码展示了如何使用一维数组进行空间优化,以解决带有冷却期的股票买卖问题。目标是在遵循以下规则的情况下,计算出最大的可能利润:
- 你可以在任何时候买入一支股票。
- 你只能在买入后某一天卖出该股票。
- 卖出股票后,你无法在接下来的一天买入股票(即冷却期)。
代码解析
状态定义
dp[0]
:表示在第i
天结束时,手中持有股票的最大利润。dp[1]
:表示在第i
天结束时,处于冷却期但不持有股票的最大利润。dp[2]
:表示在第i
天结束时,刚刚卖出股票(不再处于冷却期)的最大利润。dp[3]
:表示在第i
天结束时,处于冷却期的最大利润,与dp[1]
类似,但在上下文中,它代表了前一时刻卖出股票后的状态。
初始化
dp[0] = -prices[0]
:表示在第0天买入股票的初始状态。dp[1] = 0
:表示在第0天没有进行任何操作,不持有股票且不在冷却期。
动态转移方程
循环从 i = 1
到 prices.length
,在每一步中:
dp[0]
:要更新为持有股票的最大利润,这包括从上一天持有股票继续持有 (dp[0]
)、从冷却期买入股票 (dp[3] - prices[i]
) 或者从非冷却期买入股票 (dp[1] - prices[i]
)。dp[1]
:更新为在冷却期但不持有股票的最大利润,这主要是从上一天的冷却期状态继续 (dp[1]
) 或者从卖出股票后进入冷却期 (dp[3]
)。dp[2]
:更新为刚卖出股票的最大利润,这来自于上一天持有股票的状态 (temp + prices[i]
),其中temp
是上一天dp[0]
的值。dp[3]
:更新为在冷却期的最大利润,这直接来自于上一天的dp[2]
,即temp1
。
返回值
最后返回所有状态中的最大值,即 Math.max(dp[3],Math.max(dp[1],dp[2]))
。
空间优化
使用一维数组 dp
来代替原本可能需要的二维数组,通过循环更新数组中的值,从而减少了空间复杂度。这种方法利用了动态规划中状态之间的依赖关系,只保留当前和前一个状态的信息,避免了存储整个序列的历史状态,实现了空间优化。
这种方法将空间复杂度从 O(n)
减少到了 O(1)
(忽略数组大小对 n
的依赖,因为数组大小是固定的),同时保持了时间复杂度为 O(n)
。
方法四:
//另一种解题思路
class Solution {public int maxProfit(int[] prices) {int[][] dp = new int[prices.length + 1][2];dp[1][0] = -prices[0];for (int i = 2; i <= prices.length; i++) {/*dp[i][0] 第i天持有股票收益;dp[i][1] 第i天不持有股票收益;情况一:第i天是冷静期,不能以dp[i-1][1]购买股票,所以以dp[i - 2][1]买股票,没问题情况二:第i天不是冷静期,理论上应该以dp[i-1][1]购买股票,但是第i天不是冷静期说明,第i-1天没有卖出股票,则dp[i-1][1]=dp[i-2][1],所以可以用dp[i-2][1]买股票,没问题*/dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i - 1]);dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i - 1]);}return dp[prices.length][1];}
}
这段代码提供了另一种解决带冷却期的股票买卖问题的动态规划方法。其核心思想在于维护两个状态:在某一天持有股票的最大利润和不持有股票的最大利润,并通过动态规划的方式逐步更新这两个状态,最终求得最大利润。下面是详细的解析:
状态定义
dp[i][0]
:表示在第i
天结束时,手中持有股票的最大利润。dp[i][1]
:表示在第i
天结束时,不持有股票的最大利润(可能是因为卖出了股票,也可能是因为在冷却期内)。
初始化
dp[1][0] = -prices[0]
:表示在第1天买入股票的初始状态,即花费了prices[0]
的成本。dp[i][1]
:对于i = 1
,默认初始化为0,表示没有进行任何操作,不持有股票。
动态转移方程
-
dp[i][0]
:表示在第i
天持有股票的最大利润,可以是:- 从上一天持有股票的状态继续持有 (
dp[i - 1][0]
)。 - 或者从两天前不持有股票的状态买入股票 (
dp[i - 2][1] - prices[i - 1]
)。这是因为卖出股票后有一日的冷却期,所以只能从两天前的不持有股票状态买入。
- 从上一天持有股票的状态继续持有 (
-
dp[i][1]
:表示在第i
天不持有股票的最大利润,可以是:- 从上一天不持有股票的状态继续不持有 (
dp[i - 1][1]
)。 - 或者从上一天持有股票的状态卖出股票 (
dp[i - 1][0] + prices[i - 1]
)。
- 从上一天不持有股票的状态继续不持有 (
返回值
最后返回 dp[prices.length][1]
,即在最后一天不持有股票的最大利润。
解释
此代码通过动态规划方式,逐日更新两种状态(持有股票和不持有股票的最大利润),并且考虑到卖出股票后的冷却期,有效地解决了问题。通过对比前一天和前前一天的状态,以及当天的股票价格,计算出在当前状态下持有或不持有股票的最大可能利润,最终找到整个序列中的最大利润。
这种方法将时间复杂度保持在 O(n)
,同时空间复杂度为 O(n)
,但由于使用的是 prices.length + 1
长度的数组,实际的空间需求与输入数据的大小线性相关。可以通过进一步的空间优化(如使用滚动数组)将其降至 O(1)
。
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中 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
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
方法一:
/*** 卖出时支付手续费* @param prices* @param fee* @return*/
public int maxProfit(int[] prices, int fee) {int len = prices.length;// 0 : 持股(买入)// 1 : 不持股(售出)// dp 定义第i天持股/不持股 所得最多现金int[][] dp = new int[len][2];dp[0][0] = -prices[0];for (int i = 1; i < len; i++) {dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);dp[i][1] = Math.max(dp[i - 1][0] + prices[i] - fee, dp[i - 1][1]);}return Math.max(dp[len - 1][0], dp[len - 1][1]);
}
这段代码提供了解决股票买卖问题的一个变体——在卖出股票时需要支付一定手续费的情况下的最优策略。目标是在遵循以下规则的情况下,计算出最大的可能利润:
- 你可以在任何时候买入一支股票。
- 你只能在买入后某一天卖出该股票。
- 卖出股票时,需要支付一笔固定的手续费。
代码解析
状态定义
dp[i][0]
:表示在第i
天结束时,手中持有股票的最大现金量(实际上是负数,表示买入股票后的亏损,因为尚未卖出)。dp[i][1]
:表示在第i
天结束时,不持有股票的最大现金量(卖出股票后扣除手续费后的金额)。
初始化
dp[0][0] = -prices[0]
:表示在第0天买入股票的初始状态,即花费了prices[0]
的成本。dp[0][1] = 0
:默认初始化为0,表示没有进行任何操作,不持有股票。
动态转移方程
循环从 i = 1
到 prices.length
,在每一步中:
dp[i][0]
:要更新为持有股票的最大现金量,这包括从上一天持有股票继续持有 (dp[i - 1][0]
) 或者从上一天不持有股票状态买入股票 (dp[i - 1][1] - prices[i]
)。dp[i][1]
:要更新为不持有股票的最大现金量,这包括从上一天卖出股票扣除手续费 (dp[i - 1][0] + prices[i] - fee
) 或者从上一天不持有股票状态继续不持有 (dp[i - 1][1]
)。
返回值
最后返回所有状态中的最大值,即 Math.max(dp[len - 1][0], dp[len - 1][1])
。然而,由于 dp[i][0]
实际上表示的是负数(买入股票的亏损),最终返回的结果应当是 dp[len - 1][1]
,因为在不持有股票的状态下才有可能得到正向的利润。
时间与空间复杂度分析
- 时间复杂度:
O(n)
,其中n
是数组prices
的长度。因为算法遍历了整个数组一次。 - 空间复杂度:
O(n)
,主要来自于dp
数组的使用。尽管实际的计算只依赖于前一个状态,可以进一步优化到O(1)
的空间复杂度,例如使用滚动数组技术。
总结
这段代码有效地解决了股票买卖问题的一种特殊情况,即卖出时需要支付手续费的问题,通过动态规划的方式逐步更新两种状态(持有股票和不持有股票的最大现金量),并最终求得最大利润。
方法二:
/*** 买入时支付手续费* @param prices* @param fee* @return*/
public int maxProfit(int[] prices, int fee) {int len = prices.length;// 0 : 持股(买入)// 1 : 不持股(售出)// dp 定义第i天持股/不持股 所得最多现金int[][] dp = new int[len][2];// 考虑买入的时候就支付手续费dp[0][0] = -prices[0] - fee;for (int i = 1; i < len; i++) {dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i] - fee);dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);}return Math.max(dp[len - 1][0], dp[len - 1][1]);
}
这段代码解决的是股票买卖问题的一个变种,其中在买入股票时需要支付一笔手续费。与之前卖出时支付手续费的情况不同,这次手续费是在买入时扣除。让我们详细解析这段代码:
状态定义
dp[i][0]
:表示在第i
天结束时,手中持有股票的最大现金量(实际上是负数,表示买入股票后的亏损,因为尚未卖出,并且已经支付了手续费)。dp[i][1]
:表示在第i
天结束时,不持有股票的最大现金量(卖出股票后所得的金额,无需考虑手续费,因为手续费是在买入时支付的)。
初始化
dp[0][0] = -prices[0] - fee
:表示在第0天买入股票并支付手续费的初始状态,即花费了prices[0]
的成本加上手续费fee
。dp[0][1] = 0
:默认初始化为0,表示没有进行任何操作,不持有股票,也没有支付手续费。
动态转移方程
循环从 i = 1
到 prices.length
,在每一步中:
dp[i][0]
:要更新为持有股票的最大现金量,这包括从上一天持有股票继续持有 (dp[i - 1][0]
) 或者从上一天不持有股票状态买入股票并支付手续费 (dp[i - 1][1] - prices[i] - fee
)。dp[i][1]
:要更新为不持有股票的最大现金量,这包括从上一天卖出股票 (dp[i - 1][0] + prices[i]
) 或者从上一天不持有股票状态继续不持有 (dp[i - 1][1]
)。
返回值
最后返回所有状态中的最大值,即 Math.max(dp[len - 1][0], dp[len - 1][1])
。然而,由于 dp[i][0]
实际上表示的是负数(买入股票的亏损,包括手续费),最终有意义的返回结果是 dp[len - 1][1]
,因为在不持有股票的状态下才有可能得到正向的利润。
时间与空间复杂度分析
- 时间复杂度:
O(n)
,其中n
是数组prices
的长度。算法遍历了整个数组一次。 - 空间复杂度:
O(n)
,主要来自于dp
数组的使用。可以进一步优化到O(1)
的空间复杂度,例如使用滚动数组技术。
总结
这段代码有效地解决了股票买卖问题的另一个特殊变种,即买入时需要支付手续费的问题,通过动态规划的方式逐步更新两种状态(持有股票和不持有股票的最大现金量),并最终求得最大利润。需要注意的是,在实际应用中,通常只关注不持有股票状态下的最大利润,因为持有股票状态下的数值是负数,表示的是亏损。
方法三:
// 一维数组优化
class Solution {public int maxProfit(int[] prices, int fee) {int[] dp = new int[2];dp[0] = -prices[0];dp[1] = 0;for (int i = 1; i <= prices.length; i++) {dp[0] = Math.max(dp[0], dp[1] - prices[i - 1]);dp[1] = Math.max(dp[1], dp[0] + prices[i - 1] - fee);}return dp[1];}
}
这段代码是对上述问题(买入时支付手续费的股票买卖问题)的进一步优化,采用了空间优化技巧,将动态规划数组从二维压缩到一维,以减少空间复杂度。下面是对这段代码的详细解析:
状态定义
dp[0]
:表示在第i
天结束时,手中持有股票的最大现金量(实际上是负数,表示买入股票后的亏损,包括手续费)。dp[1]
:表示在第i
天结束时,不持有股票的最大现金量(卖出股票后所得的金额,无需考虑手续费,因为手续费是在买入时支付的)。
初始化
dp[0] = -prices[0]
:表示在第1天买入股票并支付手续费的初始状态,即花费了prices[0]
的成本。注意,这里没有立即减去手续费,而是稍后在循环中统一处理。dp[1] = 0
:默认初始化为0,表示没有进行任何操作,不持有股票,也没有支付手续费。
动态转移方程
循环从 i = 1
到 prices.length
,在每一步中:
dp[0]
:要更新为持有股票的最大现金量,这包括从上一天持有股票继续持有 (dp[0]
) 或者从上一天不持有股票状态买入股票 (dp[1] - prices[i - 1]
)。dp[1]
:要更新为不持有股票的最大现金量,这包括从上一天卖出股票并减去手续费 (dp[0] + prices[i - 1] - fee
) 或者从上一天不持有股票状态继续不持有 (dp[1]
)。
返回值
最后返回 dp[1]
,即在最后一天不持有股票的最大现金量。这是最终的利润,因为我们关心的是最终的盈利,而不是持有股票的亏损状态。
空间优化
这段代码的关键优化点在于使用了一维数组 dp
来替代之前的二维数组。由于在更新 dp
数组中的每个元素时,只依赖于上一个状态,因此不需要存储整个历史状态。这使得空间复杂度从原来的 O(n)
减少到了 O(1)
,极大地节省了空间。
时间与空间复杂度分析
- 时间复杂度:
O(n)
,其中n
是数组prices
的长度。算法遍历了整个数组一次。 - 空间复杂度:
O(1)
,使用了一维数组dp
,无论prices
数组的长度如何,空间复杂度都是常数级别。
总结
这段代码巧妙地利用了动态规划和空间优化技巧,有效地解决了买入时支付手续费的股票买卖问题,不仅计算出了最大利润,而且在保证算法效率的同时,大幅度降低了空间复杂度。
方法四:
//使用 2*2 array
class Solution {public int maxProfit(int[] prices, int fee) {int dp[][] = new int[2][2];int len = prices.length;//[i][0] = holding the stock//[i][1] = not holding the stockdp[0][0] = -prices[0];for(int i = 1; i < len; i++){dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i] - fee);}return dp[(len - 1) % 2][1];}
}
这段代码同样是对买入股票时需支付手续费问题的解决,但采取了不同的空间优化策略,使用了二维数组 dp
的两行来存储状态,从而将空间复杂度从原始的 O(n)
降低至接近 O(1)
。下面是代码的详细解析:
状态定义
dp[i % 2][0]
:表示在第i
天结束时,手中持有股票的最大现金量(实际上是负数,表示买入股票后的亏损,包括手续费)。dp[i % 2][1]
:表示在第i
天结束时,不持有股票的最大现金量(卖出股票后所得的金额,无需考虑手续费,因为手续费是在买入时支付的)。
初始化
dp[0][0] = -prices[0]
:表示在第0天买入股票并支付手续费的初始状态,即花费了prices[0]
的成本。这里没有立即减去手续费,而是在动态转移方程中处理。dp[0][1]
:默认初始化为0,表示没有进行任何操作,不持有股票,也没有支付手续费。
动态转移方程
循环从 i = 1
到 prices.length
,在每一步中:
dp[i % 2][0]
:要更新为持有股票的最大现金量,这包括从上一天持有股票继续持有 (dp[(i - 1) % 2][0]
) 或者从上一天不持有股票状态买入股票 (dp[(i - 1) % 2][1] - prices[i]
)。dp[i % 2][1]
:要更新为不持有股票的最大现金量,这包括从上一天卖出股票并减去手续费 (dp[(i - 1) % 2][0] + prices[i] - fee
) 或者从上一天不持有股票状态继续不持有 (dp[(i - 1) % 2][1]
)。
使用 i % 2
的原因
使用 i % 2
是为了在空间上进行优化,只保留最近的两个状态,即当前状态和前一个状态。这是因为动态规划中的转移方程只依赖于前一个状态,因此没有必要存储所有过去的状态,只需在内存中交替使用两个位置来存储当前状态和前一个状态,即可达到节省空间的目的。
返回值
最后返回 dp[(len - 1) % 2][1]
,即在最后一天不持有股票的最大现金量,这是最终的利润。
空间优化效果
通过这种方式,虽然表面上看 dp
数组是二维的,但实际上只需要固定大小的存储空间,不随输入数据规模 prices.length
的变化而变化,因此可以认为空间复杂度接近 O(1)
。
总结
这段代码有效地解决了买入股票时支付手续费的股票买卖问题,并通过巧妙地使用 i % 2
来交替更新状态,达到了空间上的优化,同时保持了时间复杂度为 O(n)
,整体上是一个高效且节省资源的解决方案。