📅  最后修改于: 2023-12-03 15:39:40.197000             🧑  作者: Mango
在字符串中,找出一个最长的子串,使得该子串中的每个字符都至少出现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) 种不同的长度。