问题描述
小M正在研究数组中的子数组问题。给定一个长度为N的数组 A,编号从8到 N-1。如果一个从索引i到j的子数组包含一个出现至少《次的元素义,我们就称这个 子数组为”"疯狂子数组”。 现在,你需要计算数组 A 中所有的疯狂子数组的数量。
题目分析
我们需要计算一个数组中的“疯狂子数组”的数量。根据题目定义:
- 子数组是数组的一部分,连续的元素构成。
- 如果子数组中至少有一个元素的出现次数不小于 K,则这个子数组称为“疯狂子数组”。
输入描述:
- N: 数组长度(正整数)。
- K: 判定标准,即子数组中某个元素至少出现的次数。
- A: 一个长度为 N 的整数数组。
输出描述:
返回所有“疯狂子数组”的数量。
解题思路
暴力方法(不可取):
遍历所有可能的子数组(起点和终点的所有组合)。 对于每个子数组统计每个元素的频率,看是否有元素出现次数 ≥ K。 时间复杂度为 𝑂(𝑁3),不可行。
优化方法(滑动窗口):
- 滑动窗口可以用来高效处理子数组问题。通过动态维护子数组的边界和条件,避免不必要的重复计算。
- 在滑动窗口中用一个字典 freq 存储当前窗口内每个元素的频率。
- 窗口右边界不断右移,直到满足条件(某个元素的频率 ≥ K)。此时,窗口内所有子数组的右端点都是“疯狂子数组”,我们可以一次性统计这些子数组的数量。
代码详解
代码逻辑:
def solution(N: int, K: int, A: list) -> int:
count = 0 # 用于计数“疯狂子数组”
freq = {} # 记录滑动窗口内的频率
left = 0 # 滑动窗口的左边界
for right in range(N): # 遍历数组,右边界逐渐扩大
# 更新右边界加入的元素的频率
if A[right] in freq:
freq[A[right]] += 1
else:
freq[A[right]] = 1
# 检查当前窗口是否满足条件(某个元素频率 >= K)
while any(value >= K for value in freq.values()):
# 符合条件时,计算疯狂子数组数量
count += N - right
# 缩小窗口:移除左边界的元素,并更新频率
freq[A[left]] -= 1
if freq[A[left]] == 0: # 如果元素频率为 0,删除该元素
del freq[A[left]]
left += 1 # 左边界右移
return count # 返回最终计数
代码详解
变量初始化:
- count: 记录所有“疯狂子数组”的数量。
- freq: 字典,用于动态维护当前窗口中每个元素的频率。
- left: 窗口的左边界。
窗口右边界扩展:
- 遍历数组,通过 right 控制窗口的右边界。
- 每次新增一个元素时,将其频率加到 freq 中。
- 窗口条件维护:
- 判断窗口是否满足条件(至少一个元素的频率不小于 K)。
- 如果满足条件,说明从当前 left 到 right 的所有子数组都是“疯狂子数组”。数量为 N - right。
窗口收缩:
- 为了找到新的“疯狂子数组”,移动左边界(left),同时更新频率表 freq。
- 当窗口不再满足条件时停止。
返回结果:
- count 累积了所有符合条件的子数组数量。
时间复杂度分析
右边界循环:
- 外层 for 循环遍历数组,执行 𝑂 ( 𝑁 )。
左边界移动:
- 内层 while 循环,在整个过程中每个元素最多被加入和移出一次,累计执行 𝑂 ( 𝑁 )。
频率判断:
- 每次判断 any(value >= K for value in freq.values()) 的复杂度为 𝑂 ( 1 ) O(1)(假设字典的键数是有限的)。
综合时间复杂度为 𝑂 ( 𝑁 )。
总结
问题的本质:
- 本题是典型的滑动窗口问题,目的是动态维护窗口条件,并高效计算满足条件的子数组数量。
关键点:
- 滑动窗口的动态收缩和扩展。
- 使用字典动态维护窗口内的频率,保证时间复杂度。
优点:
- 避免了暴力枚举的重复计算,时间复杂度从 𝑂 ( 𝑁 3 )降到了 𝑂 ( 𝑁 )。
- 代码清晰、逻辑简洁,非常适合处理这类“连续区间 + 条件”的问题。
扩展思考
其他类似问题:
- 找到满足条件的最长子数组。
- 统计满足条件的子数组的起始位置。
优化空间:
- 判断 freq 是否满足条件的操作在特殊情况下可能更高效。
- 如果子数组元素范围较小,可以用数组替代字典。 通过滑动窗口和频率统计,这种问题都可以在高效的时间复杂度内解决。