题解
思路
这道题的核心在于找到所有子数组中恰好包含 ( k ) 个不同整数的子数组数。由于直接计算满足条件的子数组比较困难,题解采用了「滑动窗口法」结合「差分」来解决问题:
-
滑动窗口法的核心:
- 滑动窗口通过两个指针动态维护一个窗口,使得窗口中的子数组满足条件。
- 对于「最多包含 ( k ) 个不同整数」的子数组,可以通过扩展窗口并统计所有符合条件的子数组数实现。
-
差分技巧:
- 子数组包含「恰好 ( k ) 个不同整数」的数量 = 子数组包含「最多 ( k ) 个不同整数」的数量 - 子数组包含「最多 ( k-1 ) 个不同整数」的数量。
- 这种差分技巧有效地避免了重复统计,并将问题拆解为更简单的两个子问题。
-
实现细节:
- 用一个
count哈希表记录每个整数在当前窗口中的频率,动态维护窗口中不同整数的数量。 - 每次扩展窗口时,统计当前窗口内所有子数组数量,并在必要时收缩窗口。
- 用一个
代码详解
from collections import defaultdict
def solution(nums: list, k: int) -> int:
def at_most(k):
"""计算最多包含 k 个不同整数的子数组数量"""
count = defaultdict(int) # 记录窗口中每个数字的出现次数
left = 0 # 窗口左边界
result = 0 # 结果累计
for right in range(len(nums)):
# 扩大窗口,添加当前右边界元素
count[nums[right]] += 1
# 如果不同整数超过 k,收缩窗口
while len(count) > k:
count[nums[left]] -= 1
if count[nums[left]] == 0:
del count[nums[left]] # 删除频率为 0 的数字
left += 1
# 窗口内符合条件的子数组数量
result += right - left + 1
return result
# 恰好包含 k 个不同整数的子数组数量
return at_most(k) - at_most(k - 1)
关键点总结
-
窗口动态维护:
- 扩展窗口时添加元素,收缩窗口时删除元素。
- 窗口中不同整数的数量由
count哈希表动态维护。
-
子数组统计:
- 每次扩展窗口时,新增的子数组数量为窗口长度 ( \text{right} - \text{left} + 1 )。
-
差分技巧:
- 差分计算让我们无需单独处理「恰好 ( k ) 个不同整数」的问题,转而解决更简单的「最多 ( k ) 个不同整数」的问题。
感受
-
滑动窗口法的优雅:
- 滑动窗口是处理子数组问题的经典方法,这道题通过「窗口+差分」的设计,清晰地将复杂问题分解为更易处理的子问题。
-
时间复杂度的控制:
- 滑动窗口的时间复杂度为 ( O(n) ),因为每个指针(左右边界)都至多遍历数组一次。
- 对于这类子数组统计问题,避免 ( O(n^2) ) 的暴力枚举是滑动窗口法的巨大优势。
-
差分思路的巧妙性:
- 差分技巧让问题由复杂变简单,将「恰好包含 ( k ) 个不同整数」的问题拆解为两个「最多包含」问题,使得问题的解法非常自然流畅。
-
代码可读性高:
- 代码通过辅助函数
at_most(k)的封装,使得逻辑清晰,易于扩展和维护。
- 代码通过辅助函数
总结
- 本题非常适合用滑动窗口法解决,尤其是子数组的统计问题,滑动窗口往往能将暴力 ( O(n^2) ) 优化到 ( O(n) )。
- 差分思路是解决「恰好包含 ( k ) 个」这类问题的常见技巧,非常值得学习和掌握。
- 在实现过程中,通过哈希表维护窗口状态是一个常见而有效的手段。
这道题让我对滑动窗口法在子数组问题上的应用有了更深刻的理解,同时也认识到差分技巧在复杂问题中的简化作用,是一道非常经典且值得反复练习的题目。