大小为 K 且平均值大于等于阈值的子数组数目 图解题解
这道题到底在问什么
- 输入
- arr=[2,2,2,2,5,5,5,8], k=3, threshold=4
- 输出
- 3
- 输入
- arr=[2,2,2,2,5,5,5,8], k=4, threshold=5
- 输出
- 1
- 输入
- arr=[1,1,1,1,1,1,1,1], k=3, threshold=4
- 输出
- 0
最优解:一步一步想明白
- 3记牢两句:平均值比较换成「窗口和 ≥ k×threshold」也就是 ≥ 12;窗口滑动时和只需加右端、减左端。下面每帧都在套这两句。
- 4上面这排是 [2,2,2,2,5,5,5,8] 的 8 个数。开滑前先把阈值线定好:平均值要 ≥ 4,等价于长度 3 的窗口和要 ≥ 3 乘 4 也就是 ≥ 12。接下来从最左边装出第一个长度 3 的窗口,然后一格一格往右滑。
- 5先放第 0 个数 2,窗口里现在只有它一个,窗口和 = 2。窗口还没装满 3 个,先不忙比。
- 6再放第 1 个数 2,窗口和从 2 加到 4。还差一个数才装满。
- 7再放第 2 个数 2,窗口和从 4 加到 6。到这第一个长度 3 的窗口 [2,2,2] 装满了。
- 8第一个窗口 [2,2,2] 的和是 6,拿去跟阈值线 12 比:6 比 12 小,平均值不够 4,不计。已计数 0 个。
- 9窗口往右滑一格。右边新进来一个数 arr[3] = 2,先把它加进窗口和,和从 6 涨到 8。这会儿窗口暂时盖了 4 个数,马上要把最左边那个吐出去。
- 10把离开窗口的最左端 arr[0] = 2(标红的这个)从和里减掉,和从 8 落回 6。窗口又是规规矩矩的长度 3,覆盖第 1 到第 3 个。整个滑动只做了一加一减,没有重新求和。
- 11这个窗口的和是 6,跟阈值线 12 比:6 比 12 小,平均值不到 4,跳过。已计数 0 个。
- 12窗口往右滑一格。右边新进来一个数 arr[4] = 5,先把它加进窗口和,和从 6 涨到 11。这会儿窗口暂时盖了 4 个数,马上要把最左边那个吐出去。
- 13把离开窗口的最左端 arr[1] = 2(标红的这个)从和里减掉,和从 11 落回 9。窗口又是规规矩矩的长度 3,覆盖第 2 到第 4 个。整个滑动只做了一加一减,没有重新求和。
- 14这个窗口的和是 9,跟阈值线 12 比:9 比 12 小,平均值不到 4,跳过。已计数 0 个。
- 15窗口往右滑一格。右边新进来一个数 arr[5] = 5,先把它加进窗口和,和从 9 涨到 14。这会儿窗口暂时盖了 4 个数,马上要把最左边那个吐出去。
- 16把离开窗口的最左端 arr[2] = 2(标红的这个)从和里减掉,和从 14 落回 12。窗口又是规规矩矩的长度 3,覆盖第 3 到第 5 个。整个滑动只做了一加一减,没有重新求和。
- 17这个窗口的和是 12,跟阈值线 12 比:12 比 12 大或相等,达标,计一个,这窗口涂绿。已计数 1 个。
- 18窗口往右滑一格。右边新进来一个数 arr[6] = 5,先把它加进窗口和,和从 12 涨到 17。这会儿窗口暂时盖了 4 个数,马上要把最左边那个吐出去。
- 19把离开窗口的最左端 arr[3] = 2(标红的这个)从和里减掉,和从 17 落回 15。窗口又是规规矩矩的长度 3,覆盖第 4 到第 6 个。整个滑动只做了一加一减,没有重新求和。
- 20这个窗口的和是 15,跟阈值线 12 比:15 比 12 大或相等,达标,计一个,这窗口涂绿。已计数 2 个。
- 21窗口往右滑一格。右边新进来一个数 arr[7] = 8,先把它加进窗口和,和从 15 涨到 23。这会儿窗口暂时盖了 4 个数,马上要把最左边那个吐出去。
- 22把离开窗口的最左端 arr[4] = 5(标红的这个)从和里减掉,和从 23 落回 18。窗口又是规规矩矩的长度 3,覆盖第 5 到第 7 个。整个滑动只做了一加一减,没有重新求和。
- 23这个窗口的和是 18,跟阈值线 12 比:18 比 12 大或相等,达标,计一个,这窗口涂绿。已计数 3 个。
- 24窗口从左滑到右,一共 6 个长度 3 的窗口都比过了。达标的是 [2,5,5]、[5,5,5]、[5,5,8] 这三个,它们的和分别是 12、15、18,都不小于阈值线 12。所以平均值大于等于 4 的窗口有 3 个,跟开头说的答案对上了。
⚠️ 容易写错的地方
✗ 错:每个窗口都把里面 k 个数重新加一遍求平均
✓ 对:只在第一个窗口完整求和,之后滑动时加右端、减左端
相邻两个窗口只差最右和最左一个数,重算整段会把复杂度推到 O(n×k);一加一减让每步是常数,整体才 O(n)
✗ 错:真去算平均值 sum 除以 k 再跟 threshold 比
✓ 对:把阈值线乘成 k×threshold,直接拿窗口和去比
除法会引入浮点和精度误差,例 [11,13,17] 平均值不是整数;两边乘 k 后全是整数比较,既快又没有精度坑
✗ 错:把条件当成平均值严格大于 threshold
✓ 对:是大于等于,平均值正好等于阈值也算
题目要求平均值 ≥ threshold,所以窗口和 ≥ k×threshold 用的是 ≥;像 [2,5,5] 平均值正好 4 也要计进答案,写成 > 会漏掉它
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
threshold *= k
s = sum(arr[:k])
ans = int(s >= threshold)
for i in range(k, len(arr)):
s += arr[i] - arr[i - k]
ans += int(s >= threshold)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold) {
threshold *= k;
int s = accumulate(arr.begin(), arr.begin() + k, 0);
int ans = s >= threshold;
for (int i = k; i < arr.size(); ++i) {
s += arr[i] - arr[i - k];
ans += s >= threshold;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int numOfSubarrays(int[] arr, int k, int threshold) {
threshold *= k;
int s = 0;
for (int i = 0; i < k; ++i) {
s += arr[i];
}
int ans = s >= threshold ? 1 : 0;
for (int i = k; i < arr.length; ++i) {
s += arr[i] - arr[i - k];
ans += s >= threshold ? 1 : 0;
}
return ans;
}
}复杂度
时间
O(n)
n 为数组长度。求第一个窗口的和看 k 个数,之后每滑一步只做一次加、一次减、一次比较,每个元素总共进出窗口各一次,整体是线性扫描
空间
O(1)
自始至终只用了窗口和 s 与计数器 ans 两个整数,没有额外的数组或队列,峰值占用是常数,跟数组多长无关
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 大小为 K 且平均值大于等于阈值的子数组数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么定长窗口能做到 O(n),而不是每个窗口都求一遍和?+
因为相邻窗口高度重叠,长度 k 的窗口往右滑一格,只在最右多进一个数、最左少出一个数,中间 k 减 1 个数完全没变。所以新窗口和等于旧窗口和加右端再减左端,是常数时间。每个元素一生只进窗口一次、出窗口一次,总共 O(n)。要是每个窗口都把 k 个数重加一遍,就变成 O(n×k) 了。
为什么把比较从「平均值 ≥ threshold」改成「窗口和 ≥ k×threshold」?+
平均值是窗口和除以 k,直接比就要做除法,结果可能是小数,带来浮点精度问题,比如 11 加 13 加 17 除以 3 不是整数。把不等式两边同乘正数 k,方向不变,就变成窗口和与 k 乘 threshold 比大小,全是整数,既快又准。注意是 ≥,平均值正好等于阈值的窗口也要算上。
窗口和会不会溢出,这题的复杂度是多少?+
题目里每个数最多 1 万,数组最长 10 万,窗口和最大约十亿,没超过 32 位 int 的二十一亿上限,所以参考代码用 int 是安全的。复杂度方面,时间 O(n) 一遍扫描,空间 O(1) 只用一个窗口和和一个计数器。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 大小为 K 且平均值大于等于阈值的子数组数目 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。