13

leetcode题解:秋叶收藏集

 3 years ago
source link: https://mp.weixin.qq.com/s/QapOZbUNwRs4m_W6WIFDZw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

leetcode题解:秋叶收藏集

Original felix021 felix021 9/19
收录于话题
#动态规划 1
#leetcode 1
Image

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

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

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


- 题 -

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

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

输入:leaves = "rrryyyrryyyrr"

输出:2

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

输入: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") == 0print minimumOperations("rrr") == 1print minimumOperations("yry") == 3print minimumOperations("yrry") == 3print minimumOperations("yryry") == 2print minimumOperations("rrryyyryyyrr") == 1print minimumOperations("rrryyyrryyyrr") == 2print minimumOperations("ryrrrrrrrrryr") == 1

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

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

640?wx_fmt=jpeg


- 解² -

前述 解³ 的原始算法时间复杂度其实是 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²),那就抱着侥幸的心理提交试试:

640?wx_fmt=png

然后就又被教做人了。

640?wx_fmt=jpeg


- 解¹ -

解² 已经把能缓存的都缓存了,枚举 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)

于是问题就这么解决了?

640?wx_fmt=jpeg

需要注意的是: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 -

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

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


640?wx_fmt=png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK