阅读 1539

前端工程师的 LeetCode 之旅 -- 周赛 201




01
整理字符串



题目描述【Easy】


给你一个由大小写英文字母组成的字符串 s 。
一个整理好的字符串中,两个相邻字符 s[i] 和 s[i + 1] 不会同时满足下述条件:
0 <= i <= s.length - 2
s[i] 是小写字符,但 s[i + 1] 是相同的大写字符;反之亦然 。
请你将字符串整理好,每次你都可以从字符串中选出满足上述条件的 两个相邻 字符并删除,直到字符串整理好为止。

请返回整理好的 字符串 。题目保证在给出的约束条件下,测试样例对应的答案是唯一的。
注意:空字符串也属于整理好的字符串,尽管其中没有任何字符。
示例:
输入:s = 'leEeetcode'
输出:'leetcode'
解释:无论你是选择 i = 1 或者 i = 2,s 都会缩减为 'leetcode'



本题主要考察以下两点:

  • 数据结构 -- 栈

  • 利用 charCode 的差值来判断相邻两字符是否互为大小写字符

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

  const makeGood = function(s{
    const stack = [];
    let startIndex = 0;
    while (startIndex < s.length) {
      const currentItem = s[startIndex];
      if (stack.length && Math.abs(currentItem.charCodeAt(0) - stack[stack.length - 1].charCodeAt(0)) === 32) {
        stack.pop();
      } else {
        stack.push(currentItem);
      }
      startIndex++;
    }

    return stack.join('');
  };
复制代码



02
找出第N个字符串中的第K位



题目描述【Medium】


给你两个正整数 n 和 k,二进制字符串 Sn 的形成规则如下:
S1 = '0'
当 i > 1 时,Si = Si-1 + '1' + reverse(invert(Si-1))
其中 + 表示串联操作,reverse(x) 返回反转 x 后得到的字符串,而 invert(x) 则会翻转 x 中的每一位(0 变为 1,而 1 变为 0)
题目数据 保证 游戏存在赢家。
例如,符合上述描述的序列的前 4 个字符串依次是:
S1 = '0'
S2 = '011'
S3 = '0111001'
S4 = '011100110110001'
请你返回 Sn 的 第 k 位字符 ,题目数据保证 k 一定在 Sn 长度范围以内。
示例:
输入:n = 3,k = 1
输出:'0'
解释:s3 为 '0111001',其第一位为 '0'。



读完本题之后,应该能够发现以下规律:

  • 每一行的字符串的长度为 2^n - 1

  • 后一行与前一行是存在递推关系

  • 每一行的左右两部分存在翻转关系

根据以上规律,可以计算出由第一行演变到第 n 行的过程中,第 k 位字符经历了多少次翻转,从而得出最终结果。

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

  const findKthBit = function(n, k{
    let current = n;
    let reverseCount = 0;
    if (n === 1) {
      return '0'
    }
    while (current > 1) {
      const total = 2 ** current - 1;
      const mid = Math.ceil(total / 2);
      if (k === mid) {
        if (reverseCount % 2 === 0) {
          return '1';
        }
        return '0';
      }
      if (k > mid) {
        reverseCount++;
        k = mid * 2 - k;
      }
      current--;
    }
    if (reverseCount % 2 === 0) {
      return '0';
    }
    return '1';
  };
复制代码



03
最大数目不重叠非空子数组



题目描述【Medium】


给你数组 nums 和一个整数 target。
请你返回非空不重叠子数组的最大数目,且每一个子数组的元素和为 target。
示例:
输入:nums = [1,1,1,1,1],target = 2
输出:2
解释:总共有两个不重叠子数组 [1,1] 和 [1,1]。



求解本题需要考虑以下两个问题:

  • 子数组目标和值

  • 子数组不重叠

求解子数组目标和值最朴素的方式:两次循环。采用前缀和的方式可以通过空间换时间的方式,将复杂度降低到 O(n)。

但是本题需要确保子数组不重叠,那么就需要在前缀和的基础上进行一定的改造:寻找到满足条件的子数组之后,重置当前的前缀和状态。

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

  const maxNonOverlapping = function(nums, target{
    let record = new Set();
    record.add(0);
    let ans = 0;
    let currentSum = 0;
    for (let i = 0; i < nums.length; i++) {
      currentSum += nums[i];
      if (record.has(currentSum - target)) {
        ans++;
        record.clear();
        record.add(0);
        currentSum = 0;
      } else {
        record.add(currentSum);
      }
    }
    return ans;
  };
复制代码



04
切棍子的最小成本



题目描述【Hard】


有一根长度为 n 个单位的木棍,棍上从 0 到 n 标记了若干个位置。
你可以按顺序完成切割,也可以根据需要更改切割的顺序。
每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。
返回切棍子的 最小总成本 。
示例:
输入:n = 7, cuts = [1, 3, 4, 5]
输出:16



这是一道寻找最优解的题型,比较容易想到的思路就是遍历所有方案得到最优方案。

针对本道题,那么就可以尝试 [0, n] 之间的分割点,每次分割的最小成本由以下三部分组成:

  • 当前棍子的长度

  • 分割后左半部分棍子后续切割的成本

  • 分割后右半部分棍子后续切割的成本

是不是一眼就看出来这是一个递归的关系,另外千万不要忘记递归枚举的过程中一定要利用记忆化来减少重复子问题的计算。

  const minCost = function(n, cuts{
    const cache = new Map();
    return help(0, n, cuts, cache);
  };

  function help(left, right, cuts, cache{
    const cacheKey = `${left}-${right}`;
    if (cache.has(cacheKey)) {
      return cache.get(cacheKey);
    }
    let min = Number.MAX_SAFE_INTEGER;
    for (let i = 0; i < cuts.length; i++) {
      if (cuts[i] > left && cuts[i] < right) {
        let cost = help(left, cuts[i], cuts, cache) + help(cuts[i], right, cuts, cache);
        min = Math.min(min, cost);
      }
    }
    if (min === Number.MAX_SAFE_INTEGER) {
      min = 0
    } else {
      min += right - left;
    }
    cache.set(cacheKey, min);
    return min;
  }
复制代码

本道题提示条件中,cuts 数组的长度远远小于 n ,所以上述解法还有优化的空间。

优化的解法中可以从 cuts 数组中获取枚举的下标,但是题目中给出的 cuts 数组需要做出如下改造:

  • cuts 数组是无序的,导致在遍历的过程中很难判断当前的切割点是否在合法的区间内。

  • cuts 不包含首尾下标,导致在递归过程中没有起止边界。

优化后的代码如下:

  const minCost = function(n, cuts{
    const record = new Map();
    cuts.sort((a, b) => a - b);
    cuts.unshift(0);
    cuts.push(n);
    const maxIndex = cuts.length - 1;
    return help(0, maxIndex, cuts, record);
  };

  function help(leftIndex, rightIndex, cuts, record{
    const cacheKey = `${leftIndex}-${rightIndex}`
    if (record.has(cacheKey)) {
      return record.get(cacheKey);
    }
    const left = cuts[leftIndex];
    const right = cuts[rightIndex];

    let min = Number.MAX_SAFE_INTEGER;
    for (let i = leftIndex + 1; i < rightIndex; i++) {
      const cost = help(leftIndex, i, cuts, record) + help(i, rightIndex, cuts, record);
      min = Math.min(cost, min);
    }

    if (min === Number.MAX_SAFE_INTEGER) {
      min = 0;
    } else {
      min += right - left;
    }
    record.set(cacheKey, min);
    return min;
  }
复制代码



05
往期精彩回顾