拆箱和装箱

428 阅读9分钟

分治思想

这里思想很简单,但是真正涉及到题目的时候,会发现使用的时候还是很难的。

但是基本思想还是类似的,基本上就是把大问题拆成小问题,分别解决,然后再组合起来得到结果

241. 为运算表达式设计优先级

题目描述

给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 +, - 以及 * 。

例子1

Input: "2-1-1"

output: [0, 2]

解释: ((2-1)-1) = 0
(2-(1-1)) = 2

例子2

Input: "23-45"

output: [-34, -14, -10, -10, 10]

解释: (2*(3-(4 * 5))) = -34
((2 * 3)-(4 * 5)) = -14
((2*(3-4)) * 5) = -10
(2*((3-4) * 5)) = -10
(((2 * 3)-4)* 5) = 10

思考

1 题目难度是中等的,但是感觉难度是hard

刚开始想对于不同运算符进行处理,可是思路一直没有理清楚,后来看了下题解,思路差不多,也是对于不同运算符进行处理,不过这里处理完了之后,采用相互乘得到结果

比如这里"23-45",可以根据对不同字符进行处理,可以获得以下结果

(2*(3-(45))) = -34
(2
((3-4)*5)) = -10

((23)-(45)) = -14

((2*(3-4))5) = -10
(((2
3)-4)*5) = 10

使用递归,可以参考实现1

可以发现实现1中有些字符是重复的,可以使用缓存,参考实现2

不使用递归,使用自底向上,采用动态规划解决,这里的dp[i][j] 表示在字符串中数字组成的数组中nums[i]到nums[j]的可以组成的不同结果,那么动态转移方程就是 dp[i][j] = dp[i][k] * dp[k+1][j] (k>=i&&k<j)
参考实现3

实现1

/**
 * @param {string} input
 * @return {number[]}
 */

// Runtime: 84 ms, faster than 61.84% of JavaScript online submissions for Different Ways to Add Parentheses.
// Memory Usage: 40.7 MB, less than 42.11% of JavaScript online submissions for Different Ways to Add Parentheses.
const diffWaysToCompute = (input) => {
  const ret = [];
  for (let i = 0; i < input.length; i++) {
    const charI = input.charAt(i);
    if (charI === "-" || charI === "*" || charI === "+") {
      const part1 = input.substring(0, i);
      const part2 = input.substring(i + 1);
      const part1Ret = diffWaysToCompute(part1);
      const part2Ret = diffWaysToCompute(part2);
      // console.log(part1, part1Ret);
      // console.log(part2, part2Ret);
      for (let p1 of part1Ret) {
        for (let p2 of part2Ret) {
          let c = 0;
          switch (charI) {
            case "+":
              c = p1 + p2;
              break;
            case "-":
              c = p1 - p2;
              break;
            case "*":
              c = p1 * p2;
              break;
          }
          ret.push(c);
        }
      }
      console.log(ret);
    }
  }
  if (ret.length === 0) {
    ret.push(+input);
  }
  return ret;
};
export default diffWaysToCompute;

实现2

/**
 * @param {string} input
 * @return {number[]}
 */
// Runtime: 80 ms, faster than 71.05% of JavaScript online submissions for Different Ways to Add Parentheses.
// Memory Usage: 41.1 MB, less than 34.21% of JavaScript online submissions for Different Ways to Add Parentheses.
const computeWithDP = (input, map) => {
  const res = [];
  const len = input.length;
  for (let i = 0; i < len; i++) {
    const charI = input.charAt(i);
    if (charI == "+" || charI == "-" || charI == "*") {
      const part1Res = [];
      const part2Res = [];
      const part1 = input.substring(0, i);
      const part2 = input.substring(i + 1);
      if (map.has(part1)) {
        part1Res = map.get(part1);
      } else {
        part1Res = computeWithDP(part1, map);
        map.set(part1, part1Res);
      }
      if (map.has(part2)) {
        part2Res = map.get(part2);
      } else {
        part2Res = computeWithDP(part2, map);
        map.set(part2, part2Res);
      }

      for (let res1 of part1Res) {
        for (let res2 of part2Res) {
          switch (charI) {
            case "+":
              res.push(res1 + res2);
              break;
            case "-":
              res.push(res1 - res2);
              break;
            case "*":
              res.push(res1 * res2);
              break;
            default:
              break;
          }
        }
      }
    }
  }
  if (res.length === 0) {
    res.push(+input);
  }
  return result;
};

export default (input) => {
  const map = new Map();
  return computeWithDP(input, map);
};

实现3

/**
 * @param {string} input
 * @return {number[]}
 */
// Runtime: 72 ms, faster than 94.74% of JavaScript online submissions for Different Ways to Add Parentheses.
// Memory Usage: 39 MB, less than 86.84% of JavaScript online submissions for Different Ways to Add Parentheses.
const diffWaysToCompute = (input, map) => {
  if (input.length === 0 || !input) return [];
  const oprs = [];
  const nums = [];
  let begin = 0;
  // 计算出有input中有多少个操作符和多少个数字
  for (let i = 0; i < input.length; i++) {
    const charI = input.charAt(i);
    if (charI == "+" || charI == "-" || charI == "*") {
      oprs.push(charI);
      nums.push(+input.substring(begin, i));
      begin = i + 1;
    }
  }
  // 把最后一个数字加入
  nums.push(+input.substring(begin));

  const numsLen = nums.length;
  const dp = [];
  // dp[i][j]表示input中数字nums[i]到数字nums[j]的之间的结果
  for (let i = 0; i < numsLen; i++) {
    dp[i] = [];
    for (let j = 0; j < numsLen; j++) {
      dp[i][j] = [];
    }
  }

  // 遍历已经发现的所有数字
  for (let i = 0; i < numsLen; i++) {
    // 计算0到i的结果
    for (let j = i; j >= 0; j--) {
      // 如果只是一个数字,直接加入
      if (i === j) {
        dp[j][i].push(nums[i]);
      } else {
        // dp[j][i] 等于dp[j][k]和dp[k+1][i]相乘
        for (let k = j; k < i; k += 1) {
          for (let left of dp[j][k]) {
            for (let right of dp[k + 1][i]) {
              let val = 0;
              switch (oprs[k]) {
                case "+":
                  val = left + right;
                  break;
                case "-":
                  val = left - right;
                  break;
                case "*":
                  val = left * right;
                  break;
              }
              dp[j][i].push(val);
            }
          }
        }
      }
    }
  }
  if (dp[0][numsLen - 1].length === 0) {
    dp[0][numsLen - 1].push(+input);
  }
  return dp[0][numsLen - 1];
};

export default diffWaysToCompute;

932. 漂亮数组

题目描述

对于某些固定的 N,如果数组 A 是整数 1, 2, ..., N 组成的排列,使得:

对于每个 i < j,都不存在 k 满足 i < k < j 使得 A[k] * 2 = A[i] + A[j]。

那么数组 A 是漂亮数组。

给定 N,返回任意漂亮数组 A(保证存在一个)。

例子1

Input: 4

output: [2,1,4,3]

例子2

Input: 5

output: [3,1,2,5,4]

提示:
1 <= N <= 1000

思考

1 题目本身感觉如果第一次接触到,估计应该不会想到如何拆分和如何合并。

这里比较巧妙的是把n给拆分成偶数和奇数,这样如果从偶数集合和奇数集合里边各自取一个数的时候,则肯定不会出现 A[k] * 2 = A[i]+A[j], 因为偶数和奇数相加肯定是奇数,而A[k] * 2肯定是偶数,所以永远不会相等。

这里假设有一个漂亮数组

1.1 删除

如果删除漂亮数组中的任何一个值,到最后肯定还是漂亮数组

1.2 漂亮数组加或者减去同一个值

因为我们有 A[k] * 2 != A[i] + A[j],
(A[k] + x) * 2 = A[k] * 2 + 2x != A[i] + A[j] + 2x = (A[i] + x) + (A[j] + x)

比如: [1,3,2] + 1 = [2,4,3].

1.3 漂亮数组相乘或者相除同一个值

因为我们有 A[k] * 2 != A[i] + A[j],
对于任何x!=0 (A[k] * x) * 2 = A[k] * 2 * x != (A[i] + A[j]) * x = (A[i] * x) + (A[j] * x)

比如: [1,3,2] * 2 = [2,6,4]

通过上面可以得出,如果一个数组是漂亮数组,可以删除或者加上同一个数或者相乘同一个数都还是漂亮数组

现在假设有个漂亮数组A

A1 = A * 2 - 1
A2 = A * 2
B = A1 + A2

比如

A = [2, 1, 4, 5, 3]
A1 = [3, 1, 7, 9, 5]
A2 = [4, 2, 8, 10, 6]
B = A1 + A2 = [3, 1, 7, 9, 5, 4, 2, 8, 10, 6]

A1肯定是漂亮数组,如果从A1中任意选择一个,那么从A1中选择一个和从A2中选择一个,肯定还是不存在 A[k] * 2 === A[i] + A[j]

分治参考实现1

自顶向下参考实现2

字底向上参考实现3

实现1

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

// Runtime: 84 ms, faster than 81.25% of JavaScript online submissions for Beautiful Array.
// Memory Usage: 40.1 MB, less than 87.50% of JavaScript online submissions for Beautiful Array.
export default (N) => {
  let res = [];
  res.push(1);
  while (res.length < N) {
    const tmp = [];
    for (let odd of res) {
      if (odd * 2 - 1 <= N) {
        tmp.push(odd * 2 - 1);
      }
    }
    for (let even of res) {
      if (even * 2 <= N) {
        tmp.push(even * 2);
      }
    }
    res = tmp;
  }
  return res;
};

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

实现2

/**
 * @param {number} N
 * @return {number[]}
 */
// Runtime: 84 ms, faster than 81.25% of JavaScript online submissions for Beautiful Array.
// Memory Usage: 40.6 MB, less than 43.75% of JavaScript online submissions for Beautiful Array.
const getBeautifulArray = (N, map) => {
  if (N === 1) {
    map.set(1, [1]);
    return [1];
  }
  if (N === 2) {
    map.set(2, [1, 2]);
    return [1, 2];
  }
  const left = Math.floor(N / 2);
  let leftArr = [];
  if (map.has(left)) {
    leftArr = map.get(left);
  } else {
    leftArr = getBeautifulArray(left, map);
    map.set(left, leftArr);
  }
  let rightArr = [];
  const right = N - left;
  if (map.has(right)) {
    rightArr = map.get(right);
  } else {
    rightArr = getBeautifulArray(right, map);
    map.set(right, rightArr);
  }
  leftArr = leftArr.map((x) => x * 2);
  rightArr = rightArr.map((x) => x * 2 - 1);
  const temp = [...leftArr, ...rightArr];
  map.set(N, temp);
  return temp;
};

export default (N) => {
  if (N === 1) return [1];
  const map = new Map();
  return getBeautifulArray(N, map);
};

实现3

/**
 * @param {number} N
 * @return {number[]}
 */
// Runtime: 156 ms, faster than 6.25% of JavaScript online submissions for Beautiful Array.
// Memory Usage: 57.3 MB, less than 6.25% of JavaScript online submissions for Beautiful Array.
export default (N) => {
  if (N === 1) return [1];
  const dp = [];
  dp[1] = [1];
  dp[2] = [1, 2];

  for (let i = 3; i <= N; i++) {
    const left = Math.floor(i / 2);
    const leftArr = dp[left].map((x) => x * 2);
    const right = i - left;
    const rightArr = dp[right].map((x) => x * 2 - 1);
    dp[i] = [...leftArr, ...rightArr];
  }
  return dp[N];
};

312. 戳气球

题目描述

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

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

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

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

例子1

Input: [3,1,5,8]

output: 167

解释:nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167

例子2

Input: nums = [1,5]

output: 10

思考

1 题目本身还是有点难度的,如果第一次接触,考虑下直接看题解就可以


这里比较难的是如何把数组长度为n,拆分成两个数组。

因为这里需要我们每次戳破一个气球,然后得到相连的三个数组的硬币

如果采用拆分和组合的方式,可以按照下面考虑

1.1 首先考虑一下扎[0,nums.length-1] 这个区间可以得到的最大硬币,也就是最后结果

1.2 因为我们需要每次扎一个气球,假设我们从正面考虑,第一次先扎第i位置的气球,这个时候可以拆分成[0, i-1],nums[i],[i+1, nums.length-1] 这三个区间,但是很明显这里不能这么划分,因为如果按照这样划分,那么nums[i]左边和右边相连的数字是不确定的。这个时候如果逆向思考一下,如果是最后扎i位置的气球呢,很明显这时候也拆分成了三个区间 [0, i-1],nums[i],[i+1, nums.length-1],但是这时候因为是最后扎i位置的气球,可以看到nums[i]左边和右边的位置是确定的

1.3 剩下的就是定义区间的结果,假设dp[i][j]表示从i到j扎气球的可以得到的最大硬币数目(不包括i和j),但是这里可以注意到提示
你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。

这时候我们可以重新组合下nums成为copynums,假设nums的长度是len,那么copynums的长度是len+2
copynums[0]=1,copynums[len+1]=1,copynums[1...len] = nums[0...len-1]

根据前面dp[i][j]的定义,那么我们的结果就变成了求copynums的dp[0][len+1]了

1.4 最后就剩下如何拆分再如何合并了,因为要求dp[0][len+1]的值,因为在copynums从1到len任何一个位置都可能是最后一次扎破,所以肯定得循环从1到len。假设其中一次我们选择扎破i位置的气球,那么此时的dp[0][len+1]等于什么呢?

此时dp[0][len+1] = dp[0][i]+dp[i][len+1]+copynums[0]* copynums[i]* copynums[len+1]

因为i位置的气球是最后扎破的,那么可以肯定此时的得到的最大硬币是copynums[0]* copynums[i]* copynums[len+1]

此时左边的最大硬币数是dp[0][i],因为可以选择从1到i-1的任何一个气球最后扎破,所以i位置的气球可以作为dp[0][i]的任何一个位置的最右边。

右边同理

因为i的范围是从1到len,所以最后的结果是要取这些所有可能的最大值

比如copynums = [1,3,1,5,8,1]的时候,选择i=3的时候, dp[0][5] = dp[0][3]+dp[3][5]+1 * 1 * 5

可以按照dp[i][j]的定义,看看是不是已经包括了所有的可能情况

分治参考实现1

从下向上参考实现2

实现1

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

const burst = (memo, nums, left, right) => {
  if (left + 1 === right) return 0;
  if (memo[left][right] > 0) return memo[left][right];
  let ans = 0;
  for (let i = left + 1; i < right; i++) {
    ans = Math.max(ans, nums[left] * nums[i] * nums[right] + burst(memo, nums, left, i) + burst(memo, nums, i, right));
  }
  memo[left][right] = ans;
  return ans;
};

// Runtime: 448 ms, faster than 5.06% of JavaScript online submissions for Burst Balloons.
// Memory Usage: 40.1 MB, less than 89.40% of JavaScript online submissions for Burst Balloons.
export default (nums) => {
  const len = nums.length;
  const copyNums = new Array(len + 2);
  copyNums[0] = 1;
  copyNums[len + 1] = 1;
  for (let i = 0; i < len; i++) {
    copyNums[i + 1] = nums[i];
  }

  const memo = [];
  for (let i = 0; i < len + 2; i++) {
    memo[i] = new Array(len + 2).fill(0);
  }
  // console.log(memo);
  return burst(memo, copyNums, 0, len + 1);
};


实现2

const n = nums.length;
  nums = [1, ...nums, 1];
  const len = nums.length;
  const dp = [];
  for (let i = 0; i < len; i++) {
    dp[i] = new Array(len).fill(0);
  }
  // dp[i][j] 表示i到j-1的最大值
  // i2jLen 表示dp[i][j]表示的数组长度
  for (let i2jLen = 1; i2jLen <= n; i2jLen++) {
    for (let i = 0, j = i2jLen + 1; j <= len - 1; i++, 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];