代码随想录算法训练营day50
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4] 输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。 随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1] 输出:0
提示:
css
复制代码
1 <= prices.length <= 105 0 <= prices[i] <= 105
来源:力扣(LeetCode) 链接:leetcode.cn/problems/be…
方法一:动态规划
思路与算法
由于我们最多可以完成两笔交易,因此在任意一天结束之后,我们会处于以下五个状态中的一种:
复制代码
未进行过任何操作; 只进行过一次买操作; 进行了一次买操作和一次卖操作,即完成了一笔交易; 在完成了一笔交易的前提下,进行了第二次买操作; 完成了全部两笔交易。
由于第一个状态的利润显然为 00,因此我们可以不用将其记录。对于剩下的四个状态,我们分别将它们的最大利润记为 buy1buy1,sell1sell1,buy2buy2 以及 sell2sell2。
如果我们知道了第 i−1i−1 天结束后的这四个状态,那么如何通过状态转移方程得到第 ii 天结束后的这四个状态呢?
对于 buy1buy1 而言,在第 ii 天我们可以不进行任何操作,保持不变,也可以在未进行任何操作的前提下以 prices[i]prices[i] 的价格买入股票,那么 buy1buy1 的状态转移方程即为:
buy1=max{buy1′,−prices[i]}buy1=max{buy1′,−prices[i]}
这里我们用 buy1′buy1′ 表示第 i−1i−1 天的状态,以便于和第 ii 天的状态 buy1buy1 进行区分。
对于 sell1sell1 而言,在第 ii 天我们可以不进行任何操作,保持不变,也可以在只进行过一次买操作的前提下以 prices[i]prices[i] 的价格卖出股票,那么 sell1sell1 的状态转移方程即为:
sell1=max{sell1′,buy1′+prices[i]}sell1=max{sell1′,buy1′+prices[i]}
同理我们可以得到 buy2buy2 和 sell2sell2 对应的状态转移方程:
buy2=max{buy2′,sell1′−prices[i]}sell2=max{sell2′,buy2′+prices[i]}buy2=max{buy2′,sell1′−prices[i]}sell2=max{sell2′,buy2′+prices[i]}
在考虑边界条件时,我们需要理解下面的这个事实:
复制代码
无论题目中是否允许「在同一天买入并且卖出」这一操作,最终的答案都不会受到影响,这是因为这一操作带来的收益为零。
因此,在状态转移时,我们可以直接写成:
{buy1=max{buy1,−prices[i]}sell1=max{sell1,buy1+prices[i]}buy2=max{buy2,sell1−prices[i]}sell2=max{sell2,buy2+prices[i]}⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧buy1=max{buy1,−prices[i]}sell1=max{sell1,buy1+prices[i]}buy2=max{buy2,sell1−prices[i]}sell2=max{sell2,buy2+prices[i]}
例如在计算 sell1sell1 时,我们直接使用 buy1buy1 而不是 buy1′buy1′ 进行转移。buy1buy1 比 buy1′buy1′ 多考虑的是在第 ii 天买入股票的情况,而转移到 sell1sell1 时,考虑的是在第 ii 天卖出股票的情况,这样在同一天买入并且卖出收益为零,不会对答案产生影响。同理对于 buy2buy2 以及 sell2sell2,我们同样可以直接根据第 ii 天计算出的值来进行状态转移。
那么对于边界条件,我们考虑第 i=0i=0 天时的四个状态:buy1buy1 即为以 prices[0]prices[0] 的价格买入股票,因此 buy1=−prices[0]buy1=−prices[0];sell1sell1 即为在同一天买入并且卖出,因此 sell1=0sell1=0;buy2buy2 即为在同一天买入并且卖出后再以 prices[0]prices[0] 的价格买入股票,因此 buy2=−prices[0]buy2=−prices[0];同理可得 sell2=0sell2=0。我们将这四个状态作为边界条件,从 i=1i=1 开始进行动态规划,即可得到答案。
在动态规划结束后,由于我们可以进行不超过两笔交易,因此最终的答案在 00,sell1sell1,sell2sell2 中,且为三者中的最大值。然而我们可以发现,由于在边界条件中 sell1sell1 和 sell2sell2 的值已经为 00,并且在状态转移的过程中我们维护的是最大值,因此 sell1sell1 和 sell2sell2 最终一定大于等于 00。同时,如果最优的情况对应的是恰好一笔交易,那么它也会因为我们在转移时允许在同一天买入并且卖出这一宽松的条件,从 sell1sell1 转移至 sell2sell2,因此最终的答案即为 sell2sell2。
java
复制代码
class Solution { public int maxProfit(int[] prices) { int n = prices.length; int buy1 = -prices[0], sell1 = 0; int buy2 = -prices[0], sell2 = 0; for (int i = 1; i < n; ++i) { buy1 = Math.max(buy1, -prices[i]); sell1 = Math.max(sell1, buy1 + prices[i]); buy2 = Math.max(buy2, sell1 - prices[i]); sell2 = Math.max(sell2, buy2 + prices[i]); } return sell2; } }
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格,和一个整型 k 。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 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 。
提示:
yaml
复制代码
0 <= k <= 100 0 <= prices.length <= 1000 0 <= prices[i] <= 1000
来源:力扣(LeetCode) 链接:leetcode.cn/problems/be…
方法一:动态规划
思路与算法
与其余的股票问题类似,我们使用一系列变量存储「买入」的状态,再用一系列变量存储「卖出」的状态,通过动态规划的方法即可解决本题。
我们用 buy[i][j]buy[i][j] 表示对于数组 prices[0..i]prices[0..i] 中的价格而言,进行恰好 jj 笔交易,并且当前手上持有一支股票,这种情况下的最大利润;用 sell[i][j]sell[i][j] 表示恰好进行 jj 笔交易,并且当前手上不持有股票,这种情况下的最大利润。
那么我们可以对状态转移方程进行推导。对于 buy[i][j]buy[i][j],我们考虑当前手上持有的股票是否是在第 ii 天买入的。如果是第 ii 天买入的,那么在第 i−1i−1 天时,我们手上不持有股票,对应状态 sell[i−1][j]sell[i−1][j],并且需要扣除 prices[i]prices[i] 的买入花费;如果不是第 ii 天买入的,那么在第 i−1i−1 天时,我们手上持有股票,对应状态 buy[i−1][j]buy[i−1][j]。那么我们可以得到状态转移方程:
buy[i][j]=max{buy[i−1][j],sell[i−1][j]−price[i]}buy[i][j]=max{buy[i−1][j],sell[i−1][j]−price[i]}
同理对于 sell[i][j]sell[i][j],如果是第 ii 天卖出的,那么在第 i−1i−1 天时,我们手上持有股票,对应状态 buy[i−1][j−1]buy[i−1][j−1],并且需要增加 prices[i]prices[i] 的卖出收益;如果不是第 ii 天卖出的,那么在第 i−1i−1 天时,我们手上不持有股票,对应状态 sell[i−1][j]sell[i−1][j]。那么我们可以得到状态转移方程:
sell[i][j]=max{sell[i−1][j],buy[i−1][j−1]+price[i]}sell[i][j]=max{sell[i−1][j],buy[i−1][j−1]+price[i]}
由于在所有的 nn 天结束后,手上不持有股票对应的最大利润一定是严格由于手上持有股票对应的最大利润的,然而完成的交易数并不是越多越好(例如数组 pricesprices 单调递减,我们不进行任何交易才是最优的),因此最终的答案即为 sell[n−1][0..k]sell[n−1][0..k] 中的最大值。
细节
在上述的状态转移方程中,确定边界条件是非常重要的步骤。我们可以考虑将所有的 buy[0][0..k]buy[0][0..k] 以及 sell[0][0..k]sell[0][0..k] 设置为边界。
对于 buy[0][0..k]buy[0][0..k],由于只有 prices[0]prices[0] 唯一的股价,因此我们不可能进行过任何交易,那么我们可以将所有的 buy[0][1..k]buy[0][1..k] 设置为一个非常小的值,表示不合法的状态。而对于 buy[0][0]buy[0][0],它的值为 −prices[0]−prices[0],即「我们在第 00 天以 prices[0]prices[0] 的价格买入股票」是唯一满足手上持有股票的方法。
对于 sell[0][0..k]sell[0][0..k],同理我们可以将所有的 sell[0][1..k]sell[0][1..k] 设置为一个非常小的值,表示不合法的状态。而对于 sell[0][0]sell[0][0],它的值为 00,即「我们在第 00 天不做任何事」是唯一满足手上不持有股票的方法。
在设置完边界之后,我们就可以使用二重循环,在 i∈[1,n),j∈[0,k]i∈[1,n),j∈[0,k] 的范围内进行状态转移。需要注意的是,sell[i][j]sell[i][j] 的状态转移方程中包含 buy[i−1][j−1]buy[i−1][j−1],在 j=0j=0 时其表示不合法的状态,因此在 j=0j=0 时,我们无需对 sell[i][j]sell[i][j] 进行转移,让其保持值为 00 即可。
最后需要注意的是,本题中 kk 的最大值可以达到 109109,然而这是毫无意义的,因为 nn 天最多只能进行 ⌊n2⌋⌊2n⌋ 笔交易,其中 ⌊x⌋⌊x⌋ 表示对 xx 向下取整。因此我们可以将 kk 对 ⌊n2⌋⌊2n⌋ 取较小值之后再进行动态规划。
java
复制代码
class Solution { public int maxProfit(int k, int[] prices) { if (prices.length == 0) { return 0; } int n = prices.length; k = Math.min(k, n / 2); int[][] buy = new int[n][k + 1]; int[][] sell = new int[n][k + 1]; buy[0][0] = -prices[0]; sell[0][0] = 0; for (int i = 1; i <= k; ++i) { buy[0][i] = sell[0][i] = Integer.MIN_VALUE / 2; } for (int i = 1; i < n; ++i) { buy[i][0] = Math.max(buy[i - 1][0], sell[i - 1][0] - prices[i]); for (int j = 1; j <= k; ++j) { buy[i][j] = Math.max(buy[i - 1][j], sell[i - 1][j] - prices[i]); sell[i][j] = Math.max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]); } } return Arrays.stream(sell[n - 1]).max().getAsInt(); } }