鄙厂和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 -
这么有区分度的题,好像很适合面试,只可惜题面描述起来有点困难,还是算了。
有什么不理解的地方欢迎留言探讨,喜欢的话记得点“在看”,感谢阅读~