【算法-LeetCode】122. 买卖股票的最佳时机 II(动态规划;贪心)

122. 买卖股票的最佳时机 II - 力扣(LeetCode)

文章更新:2021年10月15日14:58:27

问题描述及示例

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 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。

提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的题解

此前做过一个买卖股票的问题,可做参考:

参考:【算法-LeetCode】121. 买卖股票的最佳时机(动态规划;贪心)_赖念安的博客-CSDN博客

本题和上面的这道题目的唯一区别就是本题可以多次买卖股票,而上面这道题目只能买卖一次。只要明白这个区别,那本题就没有什么太大的问题了。具体的思路和上题基本一样,代码也没有多少差别。

我的题解1(动态规划)

因为上面的题中我没有详细的描述过程,这次我就详细描述一下吧。有关动态规划的总体思路总结可以先看下面的这篇博客:

参考:【算法-LeetCode】53. 最大子序和(动态规划初体验)_赖念安的博客-CSDN博客

本题的动态规划思路也是遵循上面所说的步骤。

首先是明白 dp 数组的含义,然后推导状态转移方程,推导的同时往往也能顺便确定初始化操作。之后就是确定遍历输入数组的顺序了。

本题的 dp 数组比较特别,是一个二维数组,且第二维的长度固定为 2。所以我们就是要在遍历 prices 数组时求 dp[i][0]dp[i][1] 这两个元素。其中:

  • dp[i][0]代表第 i 天(今天)的持股状态为不持有股票时,我的账户中剩余多少金额。而导致第 i 天不持有股票的情况有两种:
  1. 前一天(也就是第 i-1 天),我的手里就已经是不持有股票的状态,而今天如果我的手里还是没有持有股票,说明我今天压根就没买入股票(这是我主动不买入股票的行为),所以,昨天我账户里剩余多少金额,那么今天我账户里所剩的金额就还是那么多,即: dp[i][0] = dp[i-1][0]
  2. 前一天(也就是第 i-1 天),我的手里是持有股票的状态,而今天我的持股状态变为了没有持有股票,说明我今天主动卖出了股票,所以,今天我账户里所剩的金额就是在前一天的基础上加上今天出售股票所得的钱,即:dp[i][0] = dp[i-1][1] + prices[i]

题目要求获取最大利润,所以应该在两种情况中取账户剩余金额较大的那种情况,即:dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i])

  • dp[i][1]代表第 i 天(今天)的持股状态为持有股票时,我的账户中剩余多少金额。而导致第 i 天持有股票的情况也有两种:
  1. 前一天(也就是第 i-1 天),我的手里是不持有股票的状态,而今天如果我的持股状态变为了持有股票,说明我今天主动买入了股票,所以,今天我账户里所剩的金额就是在前一天的基础上减去今天买入股票所花的钱,即: dp[i][1] = dp[i-1][0] - prices[i]

    这也是和之前那道题目唯一不同的地方,因为之前那道题中是只能买卖一次,所以如果这里我主动买入了的话,那肯定是第一次买入,所以这里的状态转移方程不能加上此前积累的利润(事实上此时也没有利润可供积累),也就是:dp[i][1] = - prices[i]这里可以理解成是贷款买了股票,自己账户上还没有属于自己的利润产生。而本题是可以多次买卖的,所以必须加上此前所积累的利润。

  2. 前一天(也就是第 i-1 天),我已经是持有股票的状态,而今天我的持股状态仍然为持有股票,说明我今天啥都没干,只是把账户里的钱放在手里捂着,所以,昨天我账户里剩余多少金额,那么今天我账户里所剩的金额就还是那么多,即: dp[i][1] = dp[i-1][1]

题目要求获取最大利润,所以应该在两种情况中取账户剩余金额较大的那种情况,即:dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i])

由上面的状态转移方程可以看到,由于方程中用到了下标为 i-1 的数组元素,所以,需要对第一个 dp 数组元素进行初始化操作:

  • 第一天我不持有股票的话,我账户里所剩的金额就是 0,所以 dp[0][0] = 0
  • 第一天我持有股票的话,因为我账户里原来没有钱,所以这里可以理解为我先贷了一点款去买股票,而虽然我当前可以支配这笔贷的款,但是这钱终究不是属于自己账户的利润,所以此时我账户里所剩的金额(或者说是利润)就是 -prices[0],所以 dp[0][1] = -prices[0]

详解请看下方注释:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
  // 创建一个二维dp数组,其第一维的长度和prices的长度一致,第二维的长度固定为2
  let dp = Array.from({length: prices.length}).map(
    () => Array.from({length: 2}).fill(0)
  );
  // 初始化dp数组
  dp[0][0] = 0;
  dp[0][1] = -prices[0];
  // 开始遍历prices数组,遍历顺序是由前到后
  for(let i = 1; 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-1][0] - prices[i]);
  }
  // 返回dp数组中最后一组元素的第一维元素,因为只要把股票卖出去总是可以有一点收入的,
  // 当然总收入可能仍为负数就是了,不过本题中就不需要考虑这个问题了,最后卖出去就行了
  return dp[dp.length-1][0];
};


提交记录
200 / 200 个通过测试用例
状态:通过
执行用时:108 ms, 在所有 JavaScript 提交中击败了7.19%的用户
内存消耗:41 MB, 在所有 JavaScript 提交中击败了5.00%的用户
时间:2021/10/15 15:02

上面的二维解法还可以通过滚动数组的思想来进行空间优化。有关 dp 数组空间优化的通用方法,可以参看我下面的博客:

参考:【算法-LeetCode】1143. 最长公共子序列(动态规划;滚动数组;通用的空间优化)_赖念安的博客-CSDN博客

我的题解2(贪心)

和之前那道股票问题一样,本题也可以用贪心的方法来进行求解。

其实本题的贪心算法就是寻找 prices 数组中的所有上升区间,将这些区间中的股票差价累加起来就是我们要得的答案。如下图所示:

在这里插入图片描述

详解请看下方注释:

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
  // 如果prices的长度小于2,那么说明没有买入卖出的操作空间,不能盈利,直接返回0
  if(prices.length <= 1) {
    return 0;
  }
  // 因为下面的遍历中,利润的改变是需要一定条件触发的,
  // 为了应对prices最后一部分全为升序,所以需要特意在prices末尾增加一个标志量
  // 又因为题目中说明了prices数组中元素值的范围为 0 <= prices[i] <= 10^4^,所以压入-1
  prices.push(-1);
  // minPrice用于记录升序区间中的最低点,也就是上图中的蓝色点
  let minPrice = prices[0];
  // profit用于累积各个升序区间的股价差,最后结果就是我们想要的最大利润
  let profit = 0;
  // 开始遍历prices数组,注意是从第二个元素开始的
  for(let i = 1; i < prices.length; i++) {
    // 如果当天的股价比前一天的股价低,则需要将前一个升序区间的股价差累积下来,
    // 也就是完成了一次买入卖出的交易,并将minPrice的更新为下一个升序区间的起点值
    if(prices[i] < prices[i-1]) {
      profit += prices[i-1] - minPrice;
      minPrice = prices[i];
    }
  }
  // 最后返回所有累加的利润
  return profit;
};


提交记录
200 / 200 个通过测试用例
状态:通过
执行用时:72 ms, 在所有 JavaScript 提交中击败了72.39%的用户
内存消耗:39.7 MB, 在所有 JavaScript 提交中击败了18.51%的用户
时间:2021/10/15 15:52

注意理解上面特意在 prices 末尾压入一个 -1 的目的。

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年10月15日15:03:59

参考:买卖股票的最佳时机 II - 买卖股票的最佳时机 II - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年10月15日15:01:34
参考:【算法-LeetCode】121. 买卖股票的最佳时机(动态规划;贪心)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】53. 最大子序和(动态规划初体验)_赖念安的博客-CSDN博客
参考:【算法-LeetCode】1143. 最长公共子序列(动态规划;滚动数组;通用的空间优化)_赖念安的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值