leetcode题解:秋叶收藏集

鄙厂和leetcode联合搞了个秋季编程大赛,上周六(9月12号)是个人赛。

奖金很丰厚(一等奖 1w CNY),看着眼馋,不过和打榜的大神们差距太大,也只能看着了。

比赛一共5题,前两题太简单,后两题太难,不过第三题 “秋叶收藏集” 还挺有意思的,拉出来讲一讲。


- 题 -

小扣出去秋游,途中收集了一些红叶和黄叶,他利用这些叶子初步整理了一份秋叶收藏集 leaves, 字符串 leaves 仅包含小写字符 r 和 y, 其中字符 r 表示一片红叶,字符 y 表示一片黄叶。


出于美观整齐的考虑,小扣想要将收藏集中树叶的排列调整成「红、黄、红」三部分。每部分树叶数量可以不相等,但均需大于等于 1。每次调整操作,小扣可以将一片红叶替换成黄叶或者将一片黄叶替换成红叶。请问小扣最少需要多少次调整操作才能将秋叶收藏集调整完毕。

示例 1:

输入:leaves = "rrryyyrryyyrr"

输出:2

解释:调整两次,将中间的两片红叶替换成黄叶,得到 "rrryyyyyyyyrr"

示例 2:

输入:leaves = "ryr"

输出:0

解释:已符合要求,不需要额外操作

提示:

  • 3 <= leaves.length <= 10^5

  • leaves 中只包含字符 'r' 和字符 'y'

补充说明:这里的替换是直接把某个 r 换成 y,而不是和现有的 y 交换(即不受限于现有的 r 和 y 的数量。

先想想看,你会怎么做?


- 解³ -

最简单的做法很容易想到:对于任意 i、j ,分别计算 [0..i]、[i+1, j]、[j+1, n-1] 分别有多少个 yellow、red、yellow,加起来就是需要替换的次数;然后枚举 i、j,求最小的 f(i, j) ,就是最终答案了。

代码如下:

def minimumOperations(leaves):
  result = len(leaves)
  left_yellows = 0
  for i in range(len(leaves) - 2):
    if leaves[i] == 'y':
      left_yellows += 1
    middle_reds = 0
    for j in range(i + 1, len(leaves) - 1):
      if leaves[j] == 'r':
        middle_reds += 1
      right_yellows = 0
      for k in range(j + 1, len(leaves)):
        if leaves[k] == 'y':
          right_yellows += 1
      result = min(result, left_yellows + middle_reds + right_yellows)
  return result

用这几个简单的 case 来验证正确性:

print minimumOperations("ryr") == 0
print minimumOperations("rrr") == 1
print minimumOperations("yry") == 3
print minimumOperations("yrry") == 3
print minimumOperations("yryry") == 2
print minimumOperations("rrryyyryyyrr") == 1
print minimumOperations("rrryyyrryyyrr") == 2
print minimumOperations("ryrrrrrrrrryr") == 1

都是 True,没啥猫饼,不过问题是:太慢了

对于 10⁵ 规模的题目,O(n^3) 的解法,连提交上去碰运气的必要都没有。


- 解² -

前述 解³ 的原始算法时间复杂度其实是 n⁵ ,因为不仅需要枚举 i、j(n²),而且还应该分别遍历每一段(n³),不过具体代码在枚举 i、j 的时候顺便用 left_yellows 和 middle_reds 作为缓存,因此 [0, i] 和 [i+1, j] 这两段的值可以用 O(1) 的时间得到。

依此类推,能否也缓存 right_yellows 呢?

一旦有了思路,代码实现就很简单了:

def minimumOperations(leaves):
  right_yellows = [0] * len(leaves)
  n = 0
  for i in range(len(leaves) - 1, -1, -1):
    if leaves[i] == 'y':
      n += 1
    right_yellows[i] = n
  
  result = len(leaves)
  left_yellows = 0
  for i in range(len(leaves) - 2):
    if leaves[i] == 'y':
      left_yellows += 1
    middle_reds = 0
    for j in range(i + 1, len(leaves) - 1):
      if leaves[j] == 'r':
        middle_reds += 1
      result = min(result, left_yellows + middle_reds + right_yellows[j+1])
  return result

既然成功将时间复杂度降到了 O(n²),那就抱着侥幸的心理提交试试:

然后就又被教做人了。


- 解¹ -

解² 已经把能缓存的都缓存了,枚举 i、j 的 O(n²)无论如何没法省了,只能 摔键盘放弃 另辟蹊径。

前面的方法,本质上是把这个问题拆成 3 个小问题来解决:我们最终需要的是 "ryr" 的结构,把 "ryr" 拆成 "r"、"y"、"r"。

但这三个问题之间不是独立的:需要确定 i 才能计算 [i+1, j]、需要确定 j 才能计算 [j + 1, n-1] ;用DP术语来说,就是不满足“无后效性”。


那么我们能否把这个问题拆成更小的、互不依赖的问题来解决?

比如说,拆成 "r" 和 "yr":

  • left_r 表示左边全都是 red,可以用 O(1) 的时间(缓存)计算出

  • right_yr 表示右边由 yellow 和 red 组成,需要 O(n) 的时间计算

看起来还是 O(n²),不过值得关注的是:

  • 因为要枚举 i ,所以我们需要所有的 right_yr[0..n-1]

  • 而 right_yr[j] 可以由 right_yr[j + 1] 和 right_r[j + 1] 计算出

right_yr[j] = min(
  right_yr[j + 1], #右边是 "yr" 结构
  right_r[j + 1]   #右边是 "r" 结构
) + (1 if leaves[j] == 'r' else 0)
  # leaves[j] 是 red 的话需要换成 yellow

这意味着,我们可以先用 O(n) 的时间计算出所有 right_yr ,然后再用 O(n) 时间求出:

min(left_r(i) + right_yr(i+1) for each i)

于是问题就这么解决了?

需要注意的是:min(left_r + right_yr) 并不能得到最优解,还需要考虑 min(left_ry + right_r) 和 min(left_ry + right_yr) ,不过套路一样,细节就不展开了。

最后 AC 的代码长这样:

def minimumOperationsFast(leaves):
  n = len(leaves)
  left_r = [0] * n
  left_ry = [n] * n
  left_reds = 0
  for i in range(n):
    if leaves[i] == 'r':
      left_reds += 1
    left_r[i] = i + 1 - left_reds
    if i > 0:
      left_ry[i] = min(left_r[i-1], left_ry[i-1])
      if leaves[i] == 'r':
        left_ry[i] += 1


  right_r = [0] * n
  right_yr = [n] * n
  right_reds = 0
  for j in range(n - 1, -1, -1):
    if leaves[j] == 'r':
      right_reds += 1
    right_r[j] = n - j - right_reds
    if j < n - 1:
      right_yr[j] = min(right_r[j+1], right_yr[j+1])
      if leaves[j] == 'r':
        right_yr[j] += 1
  
  result = n
  for i in range(1, n - 1):
    result = min(
      result,
      left_r[i] + right_yr[i + 1],
      left_ry[i] + right_r[i + 1],
      left_ry[i] + right_yr[i + 1]
    )
  return result

- 1 -

这么有区分度的题,好像很适合面试,只可惜题面描述起来有点困难,还是算了。

有什么不理解的地方欢迎留言探讨,喜欢的话记得点“在看”,感谢阅读~


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值