在问 · 算法题解

395. 至少有 K 个重复字符的最长子串

中等滑动窗口枚举种类数 + 滑动窗口力扣原题 ↗

题目描述

输入一个字符串 s 和一个整数 k。

在 s 的所有子串(连续的一段)中,找出满足条件的最长子串:其中出现的每一种字符,出现次数都至少为 k。

返回这个子串的长度。如果没有任何子串满足条件,返回 0。

示例 1
输入:s = "aaabb", k = 3
输出:3

解释:"aaa" 中 a 出现 3 次。

示例 2
输入:s = "ababbc", k = 2
输出:5

解释:"ababb" 中 a 出现 2 次、b 出现 3 次。

数据范围

  • 1 <= s.length <= 10^4
  • s 只含小写英文字母
  • 1 <= k <= 10^5

解题思路:枚举种类数 + 滑动窗口

直接滑窗不行:窗口变长时“每个字符都够 k 次”这个条件既可能变好也可能变坏,不单调。办法是额外规定窗口里恰好最多有 t 种字符,t 从 1 枚举到 s 的字符种数。种类数一超过 t 就收缩 left,这样窗口的移动就单调了。每个 t 下,用 kinds 记录种类数、full 记录已达到 k 次的种类数,kinds == full 时窗口合法,更新答案。

为什么正确

对固定的 t,窗口 s[left..right] 是以 right 结尾、种类数不超过 t 的最长窗口;cnt 记录窗口内各字符次数,kinds 与 full 与 cnt 保持一致。

复杂度分析

时间 O(26·n),即 O(Σ·n),额外空间 O(Σ),Σ 为字符集大小。

易错点

不能把“所有字符都够 k 次”直接当收缩条件,它没有单调性。必须固定种类数上限 t。另外 k = 1 时同一个字符会让 kinds 和 full 同时加 1,两个判断要分开写,不能用 elif。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def longestSubstring(self, s: str, k: int) -> int:
        ans = 0
        for t in range(1, len(set(s)) + 1):
            cnt = collections.Counter()
            left = kinds = full = 0
            for right, c in enumerate(s):
                cnt[c] += 1
                if cnt[c] == 1:
                    kinds += 1
                if cnt[c] == k:
                    full += 1
                while kinds > t:
                    d = s[left]
                    if cnt[d] == k:
                        full -= 1
                    cnt[d] -= 1
                    if cnt[d] == 0:
                        kinds -= 1
                    left += 1
                if kinds == full:
                    ans = max(ans, right - left + 1)
        return ans

载入编辑器,自己跑一遍 →

其他解法

滑动窗口通过固定字符种类数让窗口具有单调性;分治利用“出现次数不足 k 的字符一定不在答案中”来切分,两者复杂度相同。

解法 2:分治 时间 O(26n),空间 O(26²)

统计区间内每个字符的出现次数。如果某个字符出现过但次数小于 k,任何合法子串都不能包含它。用这个字符把区间切成若干段,对每段递归求解并取最大值。所有字符次数都不小于 k 时,整个区间就是答案。

思路直接,代码短。每层递归至少去掉一种字符,所以递归深度不超过 26。最坏情况每层都要重新计数,常数比滑动窗口大。

class Solution:
    def longestSubstring(self, s, k):
        def dfs(l, r):
            if r - l < k:
                return 0
            cnt = [0] * 26
            for i in range(l, r):
                cnt[ord(s[i]) - 97] += 1
            split = -1
            for c in range(26):
                if 0 < cnt[c] < k:
                    split = c
                    break
            if split == -1:
                return r - l
            res = 0
            i = l
            while i < r:
                while i < r and ord(s[i]) - 97 == split:
                    i += 1
                start = i
                while i < r and ord(s[i]) - 97 != split:
                    i += 1
                res = max(res, dfs(start, i))
            return res

        return dfs(0, len(s))

滑动窗口 专题的其他题目

查看 滑动窗口 全部题目 →