LC 1520. 最多的不重叠子字符串
https://leetcode-cn.com/problems/maximum-number-of-non-overlapping-substrings/
这题出现在了 lc weekly contest 198 上,这题是第3题属于medium难度,第4题属于hard难度,但是第4题反而做出来的人比较多。我也属于这多数人中的一个。直观感觉是这题没有什么套路,另外其实这个问题有两个子问题需要解决,所以我觉得还是蛮难的。
第一个子问题就是区间扩展。假设 "babab" 这样的字符串,我们要计算“如果要覆盖a字符的话,那么最小区间什么?”这样的问题。
- 最开始包含字符a的区间是 [1,3]
- 但是[1,3]中间存在b字符,而b字符的区间是[0,4], 所以区间扩展到了[0,4]
- 如此往复直到区间不在变化
这样看上去算法似乎蛮简单的,但是当时就真的不知道如何处理,主要还是没有遇到过类似问题。
这个操作的时间复杂度如何呢?其实还是有保证的。因为我们每次遍历至少会纳入1个字符,因为只有26个字符所以最多遍历26遍。所以处理一个字符的时间复杂度是 O(26*n). 如果处理所有字符的话,那么复杂度就是 O(26 * 26 * n).
第二个子问题是,如果我们拿到了“覆盖x字符的最小区间”之后,我们如何选取“区间数量最多,区间总长度最短”的最优区间覆盖呢?这个子问题让我想起了 课程安排 问题。课程安排问题可以使用贪心算法解决,这个问题也可以,只不过贪心选择的指标不同。至于这题,贪心选择指标是:
- 假设区间表示为 (end, length), end 表示结束为止,length 表示长度
- 我们首先选择 end 最小的,如果 end 相同的话(这是有可能的,因为第一步扩展之后两个字符的区间可能完全相同,比如 "ababa"这样的情况),那么选择长度length最小的。
- 实现上我们对 (end, length) 排序,然后顺序处理,确保两个区间不会重合。
为什么我们这么选择呢?证明贪心一般使用反证法。[x1, y1] 和 [x2, y2]. 假设y1<y2,并且我们首先选择[x2,y2],如果可以将[x2,y2]替换成为[x1,y1]的话,看上去情况并不会变的更糟糕。同理可以证明长度选择的顺序。
class Solution: def maxNumOfSubstrings(self, s: str) -> List[str]: ss = [ord(x) - ord('a') for x in s] rs = [[-1, -1] for _ in range(26)] n = len(ss) for i in range(n): c = ss[i] if rs[c][0] == -1: rs[c][0] = i rs[c][1] = i ext = rs.copy() for c in range(26): p0, p1 = rs[c] if p0 == -1: continue changed = True while changed: changed = False cs = set(ss[p0:p1 + 1]) p2, p3 = p0, p1 for i in range(26): if i not in cs: continue p2 = min(p2, rs[i][0]) p3 = max(p3, rs[i][1]) if p2 != p0 or p3 != p1: changed = True p0, p1 = p2, p3 ext[c] = [p0, p1] ext = [(x, y) for x, y in ext if x != -1] ext.sort(key=lambda x: (x[1], -x[0])) st = [] for x, y in ext: if st and st[-1][1] >= x: continue st.append((x, y)) ans = [] for x, y in st: ans.append(s[x:y + 1]) ans.sort(key=lambda x: (len(x), x)) return ans