在昨天的刷题过程中,我遇到了一个非常经典且容易遗忘的题目。这道题不仅在大学期末考试中频繁出现,实习/秋招面试也是经常提及。今天这篇文章将结合我个人的解题思路和心得,复习并强化一下此题所使用的滑动窗口法。
题目:小F与特殊字符串的均衡挑战
小F无意间获得了一串特殊字符串,这个字符串的特别之处在于它只包含字符 A、S、D、F,其长度永远是 4 的倍数。这不仅让小F感到惊讶,更让他困惑的是,他需要通过尽可能少的替换,让 A、S、D、F 这四个字符在字符串中出现的频次完全相等。
一、问题解析
要使字符串满足 A、S、D、F 的频次相等,可以归结为以下条件:
- 给定字符串长度为 n,每种字符最终出现的频次应为 n / 4。
- 如果字符串原本已经满足条件,则无需替换;否则,需要通过最小化替换操作来调整字符频次
例如,对于字符串 ASDFASDFASDFASDF(长度为 16),每个字符的理想频次应为 16 / 4 = 4。如果某字符的频次超过 4,则需要替换多余的部分;而若某字符的频次不足 4,则需要通过替换增加该字符的出现次数。 关键点在于:如何以最小的替换代价,将字符串调整到满足条件。
二、窗口滑动思想的引入
解决这一问题的核心是寻找一个字符串的最小子串,并通过替换该子串,使得剩余部分直接满足要求。这是一个典型的滑动窗口问题。
- 滑动窗口的含义
滑动窗口是一种算法思想,利用两个指针在字符串上移动,维护一个区间,通过调整区间的长度和位置,寻找满足条件的最优解。
- 问题转化
对于给定字符串,可以将问题转化为:
• 找到一个最小的子串,使得移除该子串后,字符串中所有字符的频次均不超过 n / 4。
- 滑动窗口的实现步骤
• 首先计算字符串中 A、S、D、F 的初始频次;然后从左到右滑动窗口,每次检查移除窗口中的字符后,剩余部分是否满足要求;如果满足,则记录窗口的最小长度,并尝试进一步缩小窗口;否则,扩大窗口。
这种方法的核心在于,只需要一次遍历即可完成频次的动态调整,算法的时间复杂度为 O(n),适合处理较长的字符串。
三、算法设计与思路
以一个具体的例子来说明滑动窗口的设计过程:
假设字符串为 ASDFASDFFFASDFAS,其长度为 16,理想频次为 4。初始频次统计如下:
• A:4
• S:4
• D:3
• F:5
显然,字符 F 的频次超过了 4,因此需要替换多余的 F。接下来,我们通过滑动窗口找出一个最小子串,其中包含至少 1 个 F,以便移除后字符串满足条件。
具体步骤如下:
-
初始化窗口的左右指针 left 和 right,初始位置均为 0;
-
扩大窗口(移动 right 指针),更新窗口内各字符的频次;
-
当窗口内的字符频次满足条件(即剩余部分的频次均 ≤ 4)时,记录窗口长度并尝试收缩窗口(移动 left 指针);
-
重复上述过程,直到 right 遍历完整个字符串。
def solution(input_str):
from collections import defaultdict
n = len(input_str)
target = n // 4
# 统计每个字符的出现次数
count = defaultdict(int)
for c in input_str:
count[c] += 1
# 确定需要减少的字符及其多余次数
excess = {}
for c in ['A', 'S', 'D', 'F']:
if count[c] > target:
excess[c] = count[c] - target
# 如果没有多余字符,不需要替换
if not excess:
return 0
# 滑动窗口初始化
left = 0
min_length = n # 初始化为最大可能长度
window_count = defaultdict(int)
required = len(excess) # 需要满足的不同字符种类
formed = 0 # 当前满足条件的字符种类数
# 遍历字符串,右指针移动
for right in range(n):
c = input_str[right]
if c in excess:
window_count[c] += 1
# 如果当前字符的窗口计数达到了需要减少的多余次数
if window_count[c] == excess[c]:
formed += 1
# 当窗口满足所有需要减少的字符条件时,尝试缩小窗口
while left <= right and formed == required:
window_size = right - left + 1
if window_size < min_length:
min_length = window_size
# 尝试移除左边的字符,缩小窗口
left_char = input_str[left]
if left_char in excess:
window_count[left_char] -= 1
if window_count[left_char] < excess[left_char]:
formed -= 1
left += 1
return min_length
四、复杂度分析
• 时间复杂度
由于滑动窗口的每个指针最多遍历字符串一次,整个过程的时间复杂度为 O(n)。
• 空间复杂度
仅需要一个哈希表存储字符频次,空间复杂度为 O(1)。
这种方法高效且直观,适合大规模字符串处理。
五、总结
小F的挑战最终被优雅地解决,通过滑动窗口算法,我们可以快速找到一个最小子串,使得 A、S、D、F 的频次均衡。这不仅是一种巧妙的算法设计,也是数学分析和编程思维的完美结合。在实际开发中,类似的算法思想可以应用于许多优化问题,为我们的代码增添一份简洁与高效。