📌  相关文章
📜  所有字符至少出现K次的最长子字符串|套装3(1)

📅  最后修改于: 2023-12-03 15:39:40.197000             🧑  作者: Mango

所有字符至少出现K次的最长子字符串

在字符串中,找出一个最长的子串,使得该子串中的每个字符都至少出现K次。本题为套装第三篇,将研究不同的解法。

解法一:滑动窗口

滑动窗口是解决字符串子串问题的一种常用方法。我们可以先遍历一遍字符串,统计每个字符出现的次数,然后根据题意,从左往右滑动一个窗口,每次统计窗口内各个字符出现的次数。如果满足每个字符至少出现K次的条件,就更新最长子串长度。如果不满足,则左边界向右移动一位,右边界不动,继续统计。

def longestSubstring(s: str, k: int) -> int:
    res = 0
    for t in range(1, 27):
        left = 0
        right = 0
        cnt = [0] * 26  # 统计各个字符出现次数
        total = 0  # 统计窗口内不同字符数
        less = 0  # 统计出现次数小于k的字符数
        while right < len(s):
            cnt[ord(s[right]) - ord('a')] += 1
            if cnt[ord(s[right]) - ord('a')] == 1:
                total += 1
            if cnt[ord(s[right]) - ord('a')] == k:
                less -= 1
            right += 1
            while total > t:
                cnt[ord(s[left]) - ord('a')] -= 1
                if cnt[ord(s[left]) - ord('a')] == k - 1:
                    less += 1
                if cnt[ord(s[left]) - ord('a')] == 0:
                    total -= 1
                left += 1
            if less == 0:
                res = max(res, right - left)
    return res
解法二:分治法

分治法是一种递归算法,将问题分成若干个子问题进行求解,最后将子问题的解合并起来得到原问题的解。对于本题而言,我们可以先统计字符串中每个字符出现的次数,然后遍历整个字符串,找到第一个出现次数小于k的字符,将字符串分为左右两部分,分别递归求解。

def longestSubstring(s: str, k: int) -> int:
    if len(s) < k:
        return 0
    cnt = collections.Counter(s)
    for c in s:
        if cnt[c] < k:
            return max(longestSubstring(t, k) for t in s.split(c))
    return len(s)
解法三:位运算

位运算解法是一种非常巧妙的解法。我们可以遍历所有可能的字串长度,将字串中的每个字符映射到 26 个二进制位中的一位,如果出现次数小于 k,则该二进制位为 0,否则为 1。这样可以通过位运算来判断一个字串是否符合条件。具体实现可以参考下面代码。

def longestSubstring(s: str, k: int) -> int:
    n = len(s)
    ans = 0
    for length in range(1, n+1):
        mask, cnt = 0, [0] * 26
        left = 0
        for right in range(n):
            cnt[ord(s[right]) - ord('a')] += 1
            mask |= (1 << (ord(s[right]) - ord('a')))
            while cnt[ord(s[right]) - ord('a')] > k:
                cnt[ord(s[left]) - ord('a')] -= 1
                mask &= ~(1 << (ord(s[left]) - ord('a')))
                left += 1
            if mask == (1 << cnt.count(0)) - 1:
                ans = max(ans, right - left + 1)
    return ans
总结

以上三种解法,滑动窗口和分治法的时间复杂度为 O(nlogn),而位运算法的时间复杂度为 O(n^2),但实际上位运算法常数比较小,所以效率比较高。需要注意的是,在使用位运算法时,我们需要遍历所有可能的字串长度,所以时间复杂度虽然为 O(n^2),但实际上只需要遍历 O(logn) 种不同的长度。