一天一大 leet(戳气球)难度:困难-Day20200719

时间:2022-07-25
本文章向大家介绍一天一大 leet(戳气球)难度:困难-Day20200719,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目:

有 n 个气球,编号为 0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] x nums[i] x nums[right] 个硬币。这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

求所能获得硬币的最大数量。

说明:

  • 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破
  • 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

示例:

输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
     coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167

抛砖引玉

思路

  • 枚举所有可能求最大值
  • 首先起点不一定,而且起点之后路径也不确定
  • 起点可以枚举,路径就不太好枚举了
  • 那么尝试使用递归,每次传入处理后的数组再次枚举所有起点,那么就枚举了所有可能

实现

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxCoins = function (nums) {
  let _result = 0
  function maxItem(list, result) {
    if (list.length === 0) {
      _result = Math.max(_result, result)
      return
    }
    for (let i = 0; i < list.length; i++) {
      let item = (list[i - 1] || 1) * list[i] * (list[i + 1] || 1)
      maxItem(
        list.filter((val, index) => index !== i),
        result + item
      )
    }
  }
  maxItem(nums, _result)
  return _result
}

每一个起点都计算了所有可能的组合,超时也在意料之中。想到优化,优先想到的是能不能存储下已经出现过的组合,发现 i 变换的过程中左右的元素也在变化似乎没有维度可以存储。

记忆化搜索

那换个思路,不存储点,存储范围试一下:

  • 设起始点 i,结束点 j
  • dp[i][j]表示戳破 i 到 j 之间气球能得到的最大积分
  • dp 的范围:0 和 n 都参与运算则 dp(n+2)(n+2)

怎么得到 dp[i][j]的值?

  • 如果 nums 长 3 就很简单了:dp[0][2] = 0 + nums[0]*nums[1]*nums[2]* + 0 其中第一个 0 也可以用 dp[0][1]表示,第二个 0 也可以用 dp[0][3]表示 即: dp[0][2] = dp[0][1] + nums[0]*nums[1]*nums[2]* + dp[0][3]
  • 当 i 到 j 中存在大于 1 个元素时 dp[i][j]就会存在多种结果,需要枚举所有结果去最大值 设枚举 i 到 j 范围的指针为 k(k 大于 i 且 k 小于 j),则: dp[i][j] = dp[i][k] + nums[i] * nums[k] * nums[j] + dp[k][j]

实现

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxCoins = function (nums) {
  // 填充收尾默认的1,在添加后取length生成dp
  nums.unshift(1)
  nums.push(1)
  let len = nums.length,
    dp = Array.from({ length: len }, () => new Array(len).fill(-1))

  // 去除添加的两个1,i和j的范围即要求的值
  return solve(0, len - 1)

  function solve(i, j) {
    // 超出范围 返回0
    if (i >= j - 1) return 0
    // 该范围已经计算过
    if (dp[i][j] != -1) return dp[i][j]

    for (let k = i + 1; k < j; k++) {
      let sum = nums[i] * nums[k] * nums[j]
      sum += solve(i, k) + solve(k, j)
      dp[i][j] = Math.max(dp[i][j], sum)
    }
    return dp[i][j]
  }
  return dp[0][len - 1]
}

动态规划

  • 从 i 和 j 的边界开始枚举
  • 另外因为 dp[i][j]依赖 dp[i][k]、dp[k][j]
  • 即在计算 dp[i][j]时 dp[i][k]、dp[k][j],需要已知,那用实例看下,i 为 0,j 为 5,设 求 dp[1][3]k 为 2 我们需要知道 dp[1][2]、dp[2][3] (0,标识未知,1 表示 1 知)

#

3

1

5

8

3

null

dp[0][1]->0

dp[0][2]->0

dp[0][3]->0

1

null

null

dp[1][2]->1

dp[1][3]->0

5

null

null

null

dp[2][3]->1

8

null

null

null

null

会发现,i 需要从大到小变量

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxCoins = function (nums) {
  nums.unshift(1)
  nums.push(1)
  let len = nums.length,
    dp = Array.from({ length: len }, () => new Array(len).fill(0))

  // 默认填充的1不能被戳破,则i的边界为len-2-1
  // (i<j,则i最大为len-1,去除默认则未len-2-1)
  for (let i = len - 3; i >= 0; i--) {
    // i<j,则j最小为i,去除默认则未i+1
    for (let j = i + 2; j < len; j++) {
      for (let k = i + 1; k < j; k++) {
        dp[i][j] = Math.max(
          dp[i][j],
          dp[i][k] + nums[i] * nums[k] * nums[j] + dp[k][j]
        )
      }
    }
  }
  return dp[0][len - 1]
}