炒股也要动态规划?

342 阅读7分钟

股票交易

动态规划来处理解决股票交易的问题,这里涉及到使用状态机来解决多种不同状态混杂的问题。

121. 买卖股票的最佳时机

题目描述

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

例子1

Input: [7,1,5,3,6,4]

output: 5

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。


例子2

Input:[7,6,4,3,1]
output: 0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

思考

1 题目比较简单,简单的一维动态规划就可以了

实现1

/**
 * @param {number[]} prices
 * @return {number}
 */
// Runtime: 96 ms, faster than 32.40% of JavaScript online submissions for Best Time to Buy and Sell Stock.
// Memory Usage: 39.5 MB, less than 24.29% of JavaScript online submissions for Best Time to Buy and Sell Stock.
export default (prices) => {
  const n = prices.length;
  const dp = new Array(n + 1).fill(0);

  for (let i = 2; i <= n; i++) {
    let max = 0;
    for (let j = i - 2; j >= 0; j--) {
      if (prices[j] < prices[i - 1]) {
        max = Math.max(max, prices[i - 1] - prices[j]);
      } else {
        break;
      }
    }
    dp[i] = Math.max(dp[i], max, dp[i - 1]);
  }
  return dp[n];
};


时间复杂度O(n^2 ), 空间复杂度O( n)

309. 最佳买卖股票时机含冷冻期

题目描述

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

例子1

Input: [1,2,3,0,2]

output: 3

解释:对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]


思考

1 如果第一次遇见这个问题,没有接触过状态机,直接看答案就可以。
本题属于状态机解决,这里如何确定有几种状态呢?

因为这里有三种动作,sell和buy和reset,所以对应三种状态

这三种状态如何转换很容易

可以看到通过buy和sell或者reset就可以转换到各种不同的状态

这里的问题就是如何把这三种状态和我们想要的题目答案结合起来?

我们想要求出第n天的时候,可以通过买和卖股票获取的最大利润,那么第n天可以获取的最大利润和什么有关系呢?换句话说和s0和s1和s2这三种状态有什么关系呢?

应该很容易想到第n天可以获取的最大利润肯定是Math.max(s0,s2),为什么呢?

因为s1的状态是通过s0在第n天购买获得的状态或者是通过第n天休息获得,但是这第n天休息的时候,其实手里还有没有卖出的股票。

剩下的就是确定初始状态了,初始状态应该很好确定,第0天s0,s1,s2都是0,0,0,那么第一天就0,-prices[0],0

这里还要需要注意的一点就是状态机只能从一个状态到另外一个状态不能跨越

参考实现1

当然这里可以压缩空间,可以参考实现2

实现1

/**
 * @param {number[]} prices
 * @return {number}
 */

// Runtime: 92 ms, faster than 34.36% of JavaScript online submissions for Best Time to Buy and Sell Stock with Cooldown.
// Memory Usage: 41 MB, less than 9.82% of JavaScript online submissions for Best Time to Buy and Sell Stock with Cooldown.
export default (prices) => {
  const n = prices.length;
  if (n <= 1) return 0;

  // sell之后reset,reset之后reset
  const s0 = new Array(n + 1).fill(0);
  // buy之后reset,reset之后reset
  const s1 = new Array(n + 1).fill(0);
  // sell之后
  const s2 = new Array(n + 1).fill(0);
  s0[1] = 0;
  s1[1] = -prices[0];
  s2[1] = 0;

  for (let i = 2; i <= n; i++) {
    s0[i] = Math.max(s0[i - 1], s2[i - 1]);
    s1[i] = Math.max(s0[i - 1] - prices[i - 1], s1[i - 1]);
    s2[i] = s1[i - 1] + prices[i - 1];
  }
  return Math.max(s0[n], s2[n]);
};

时间复杂度O(n), 空间复杂度O(n)

实现2

/**
 * @param {number[]} prices
 * @return {number}
 */

// Runtime: 84 ms, faster than 64.42% of JavaScript online submissions for Best Time to Buy and Sell Stock with Cooldown.
// Memory Usage: 38.8 MB, less than 72.39% of JavaScript online submissions for Best Time to Buy and Sell Stock with Cooldown.
export default (prices) => {
  const n = prices.length;
  if (n <= 1) return 0;

  // sell之后reset,reset之后reset
  let s0 = 0;
  // buy之后reset,reset之后reset
  let s1 = -prices[0];
  // sell之后
  let s2 = 0;

  for (let i = 1; i < n; i++) {
    const last_s2 = s2;
    // 按照状态机顺序转换
    s2 = s1 + prices[i];
    s1 = Math.max(s0 - prices[i], s1);
    s0 = Math.max(s0, last_s2);
  }
  // console.log(s0, s2);
  return Math.max(s0, s2);
};

时间复杂度O(n), 空间复杂度O(1)

714. 买卖股票的最佳时机含手续费

题目描述

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

例子1

Input: [1, 3, 2, 8, 4, 9], fee = 2

output: 8

解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

提示:
1 0 < prices.length <= 50000.
2 0 < prices[i] < 50000.
3 0 <= fee < 50000.

思考

1 题目很简单,直接和上面一样,建立三种状态,使用状态机

这三种状态如何转换很容易

可以看到通过buy和sell就可以转换到各种不同的状态

当然这里可以压缩空间,可以参考实现1

实现1

/**
 * @param {number[]} prices
 * @param {number} fee
 * @return {number}
 */

// Runtime: 92 ms, faster than 86.05% of JavaScript online submissions for Best Time to Buy and Sell Stock with Transaction Fee.
// Memory Usage: 47.4 MB, less than 77.91% of JavaScript online submissions for Best Time to Buy and Sell Stock with Transaction Fee.
export default (prices, fee) => {
  let s0 = 0;
  let s1 = -prices[0];
  let s2 = 0;

  for (let i = 1; i < prices.length; i++) {
    let preS2 = s2;
    s2 = s1 + prices[i] - fee;
    s0 = Math.max(s0, preS2);
    s1 = Math.max(s0 - prices[i], s1);
  }
  return Math.max(s0, s2);
};


时间复杂度O(n), 空间复杂度O(1)

188. 买卖股票的最佳时机 IV

题目描述

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

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

例子1

Input: k = 2, prices = [2,4,1]

output: 2

解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

例子2

Input: k = 2, prices = [3,2,6,5,0,3]

output: 7

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

提示:
1 0 <= k <= 10^9
2 0 <= prices.length <= 1000
3 0 <= prices[i] <= 1000

思考

1 这里题目其实是比较难的

因为这里有两种状态,很明显可以想到使用二维动态规划,可是状态转移方程不是很好想出来。

刚开始做的时候,首先想到dp[i][j]表示第i次交易有j个股票的最大利润

但是一直想不出来如何得出状态转移方程

后来看了下题解,感觉也很简单,就是特别不好想出来

如果我们想要求dp[i+1][j+1],这时候有两种选择,第一种是第i+1的时候,不卖出,那么这时候 dp[i+1][j+1] = dp[i+1][j] 也就是和第j+1天的股票没有任何关系

一种是在j+1天的时候,选择卖出股票,这里有个问题,如果想在第j+1天卖出的时候,必须在0-j的时候,必须买入一只股票,否则不能卖出,所以这个时候有j种选择,比如在第t天买入股票在j+1天卖出的时候,则dp[i+1][j+1] = dp[i][t-1]+ prices[j+1]-prices[t] (t>=0&&t<=j)

可以参考下实现1

在实现1中,可以发现,下面的代码其实每次都是重复的,其实每次,我们只需要使用tmpMax的最大值就可以了,可以参考实现2

 for (let k1 = 2; k1 < j; k1++) {
        tmpMax = Math.max(tmpMax, dp[i - 1][k1] - prices[k1 - 1]);
      }

2 当然这里还有第二种解法,分别使用两个动态规划buy和sell, buy[j]表示第j次交易购买的最大利润,sell[j]表示第j次交易卖出的最大利润

动态转移很容易
buy[j] = Math.max(buy[j],sell[j-1]-prices[i])
sell[j] =Math.max(sell[j], buy[j] + prices[j])

可以参考实现3

实现1

/**
 * @param {number} k
 * @param {number[]} prices
 * @return {number}
 */
// 辅函数
const maxProfits = (prices) => {
  let maxProfit = 0;
  for (let i = 1; i < prices.length; i++) {
    if (prices[i] > prices[i - 1]) {
      maxProfit += prices[i] - prices[i - 1];
    }
  }
  return maxProfit;
};

// Runtime: 92 ms, faster than 83.33% of JavaScript online submissions for Best Time to Buy and Sell Stock IV.
// Memory Usage: 40.3 MB, less than 86.23% of JavaScript online submissions for Best Time to Buy and Sell Stock IV.
export default (k, prices) => {
  const len = prices.length;
  if (len < 2) {
    return 0;
  }
  // 如果k的天数大于len
  if (k >= len) {
    return maxProfits(prices);
  }
  const dp = [];
  for (let i = 0; i <= k; i++) {
    dp[i] = new Array(len + 1).fill(0);
  }
  for (let i = 1; i <= k; i++) {
    // 默认第一天购买
    let tmpMax = -prices[0];
    for (let j = 2; j <= len; j++) {
      // 获取在前面0-j之间购买股票的最大利润
      for (let k1 = 2; k1 < j; k1++) {
        tmpMax = Math.max(tmpMax, dp[i - 1][k1] - prices[k1 - 1]);
      }
      // 有两种选择,第一种是啥也不干dp[i][j - 1],第二种是如果在j天卖出股票的时候,必须在0到j之间购买一次
      dp[i][j] = Math.max(dp[i][j - 1], prices[j - 1] + tmpMax);
      // tmpMax = Math.max(tmpMax, dp[i - 1][j - 1] - prices[j - 1]);
    }
  }
  return dp[k][len];
};

时间复杂度O(n^2 * k), 空间复杂度O(n * k)

实现2

/**
 * @param {number} k
 * @param {number[]} prices
 * @return {number}
 */
// 辅函数
const maxProfits = (prices) => {
  let maxProfit = 0;
  for (let i = 1; i < prices.length; i++) {
    if (prices[i] > prices[i - 1]) {
      maxProfit += prices[i] - prices[i - 1];
    }
  }
  return maxProfit;
};

// Runtime: 92 ms, faster than 83.33% of JavaScript online submissions for Best Time to Buy and Sell Stock IV.
// Memory Usage: 40.3 MB, less than 86.23% of JavaScript online submissions for Best Time to Buy and Sell Stock IV.
export default (k, prices) => {
  const len = prices.length;
  if (len < 2) {
    return 0;
  }
  // 如果k的天数大于len
  if (k >= len) {
    return maxProfits(prices);
  }
  const dp = [];
  for (let i = 0; i <= k; i++) {
    dp[i] = new Array(len + 1).fill(0);
  }
  for (let i = 1; i <= k; i++) {
    // 默认第一天购买
    let tmpMax = -prices[0];
    for (let j = 2; j <= len; j++) {
      // // 获取在前面0-j之间购买股票的最大利润
      // for (let k1 = 2; k1 < j; k1++) {
      //   tmpMax = Math.max(tmpMax, dp[i - 1][k1] - prices[k1 - 1]);
      // }
      // 有两种选择,第一种是啥也不干dp[i][j - 1],第二种是如果在j天卖出股票的时候,必须在0到j之间购买一次
      dp[i][j] = Math.max(dp[i][j - 1], prices[j - 1] + tmpMax);
      tmpMax = Math.max(tmpMax, dp[i - 1][j - 1] - prices[j - 1]);
    }
  }
  return dp[k][len];
};

时间复杂度O(n * k), 空间复杂度O(n * k)

实现3

/**
 * @param {number} k
 * @param {number[]} prices
 * @return {number}
 */
// 辅函数
const maxProfits = (prices) => {
  let maxProfit = 0;
  for (let i = 1; i < prices.length; i++) {
    if (prices[i] > prices[i - 1]) {
      maxProfit += prices[i] - prices[i - 1];
    }
  }
  return maxProfit;
};

// Runtime: 84 ms, faster than 98.55% of JavaScript online submissions for Best Time to Buy and Sell Stock IV.
// Memory Usage: 39.8 MB, less than 92.03% of JavaScript online submissions for Best Time to Buy and Sell Stock IV.
export default (k, prices) => {
  const len = prices.length;
  if (len < 2) {
    return 0;
  }
  // 如果k的天数大于len
  if (k >= len) {
    return maxProfits(prices);
  }
  const buy = new Array(k + 1).fill(-Infinity);
  const sell = new Array(k + 1).fill(0);
  // buy[0] = -prices[0];
  // sell[0] = 0;
  for (let i = 1; i <= k; i++) {
    buy[i] = -prices[0];
    sell[i] = 0;
  }

  for (let i = 1; i < len; i++) {
    for (let j = 1; j <= k; j++) {
      let preBuy = buy[j];
      buy[j] = Math.max(buy[j], sell[j - 1] - prices[i]);
      sell[j] = Math.max(sell[j], preBuy + prices[i]);
    }
  }
  return sell[k];
};


时间复杂度O(n * k), 空间复杂度O(n)

炒股也要动态规划?| 创作者训练营 征文活动正在进行中......