题目描述
思路解析
一句话答案:LeetCode 2799 统计完全子数组的数目:先定整个数组的不同元素数 k 当标尺,枚举起点、右端往右扩,集合凑齐 k 后每个更长子数组都计数,时间 O(n²)、空间 O(n)。
什么样的连续子数组,才配叫「完全」
子数组是数组里连续的一段。先数出整个数组有多少种不同元素、记成 k;一段连续子数组只要也凑齐这 k 种,就叫完全子数组,要统计的是这种子数组的总个数。题面 nums=[1,3,1,2,2] 不同元素是 1、3、2 共 3 种,答案 4;nums=[5,5,5,5] 只有一种元素、k 为 1,答案 10。
每段都从头数一遍不同元素,账为什么划不来
最直白的做法是把所有子数组都摆出来:起点和终点各挑一个,约 n² 段,每段再从头扫一遍数它有几种不同元素、和 k 比。可每段单独数就要走一趟,三层叠起来是 O(n³),n 到几千就慢。慢在重复:终点只往右挪一格,前面数过的元素其实一个没变。
集合只增不减,凑齐 k 之后凭什么右边全算
把起点钉住,让右端从它一格格往右伸、元素挨个塞进同一个集合。集合只添不删,不同元素数便只增不减、始终朝一个方向往上爬、绝不回头。
于是集合大小头一回等于 k 时,k 种就全凑齐了。再往右多吞几格,新来的要么是老面孔、要么没有新种类可添,不同元素数只会卡在 k、绝不掉回去。所以从这个终点起、往右每伸一格的更长子数组都照样含齐 k 种、都是完全子数组,一个不漏。窗口一旦完整,右边全算,内层压到一趟扫完。
先把 k 定死,再枚举起点让右端加进集合
先用集合扫全数组求出 k = len(set(nums)),标尺全程不变。外层枚举起点,每换一个起点就把工作集合清空;内层让右端从当前起点往右走、逐个加进集合,大小等于 k 就给答案 ans 加一,之后继续往右扩、每格再判一次。所有起点走完,ans 就是总数。清空是关键:不清则上一轮的元素赖在集合里,大小虚高、把不该算的也算进。
[1,3,1,2,2] 里,这 4 个完全子数组怎么数出来
先求 k:不同元素 1、3、2,共 3 种。起点 0 清空集合,右端依次吞 1→大小 1、3→大小 2、1→大小仍 2、2→{1,3,2} 大小 3 第一次等于 k、ans 记 1;再吞末尾的 2→大小还是 3、ans 到 2。起点 0 数出 [0,3]、[0,4] 两段。
起点 1 清空重来,吞 3→1 种、1→2 种、2→3 种够 k、ans 到 3,再吞 2→仍 3 种、ans 到 4,数出 [1,3]、[1,4]。起点 2 只剩 1、2、2,最多 2 种、凑不齐 3;起点 3、4 元素更少,各数出 0 段。四段相加得 4,和题面答案 4 一致。
拿子数组自己的种类数当标尺,会把每段都误判成完全
最隐蔽的错是搞混标尺数谁的:k 必须是整个数组的不同元素数、算好锁死,若拿每段自己的种类数去比,它永远等于自己,所有子数组都被误判成完全。另两处:换起点忘清空集合,大小会带上一轮的元素虚高;大小刚到 k 就收手不再往右数,会漏掉更长却同样完整的子数组。
复杂度:外层 n 个起点、内层各走一趟、集合加查平均 O(1),合起来 O(n²),空间 O(n)。边界:全相同如 [5,5,5,5],k 为 1,每段都完全、答案就是子数组总数 4×5÷2=10;元素全不同只有整个数组凑得齐 k、答案 1;单元素数组答案也是 1。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记牢这套「先求 k,再枚举起点、右端往右扩,集合凑够 k 就开始逐个计数」,下面每一帧都在套它。
第 0 步 · 先求不同元素总数 k:先扫全数组,不同元素是 1、3、2,一共 k = 3 种。之后每个子数组都要拿它的不同元素个数去和这个 3 比。
起点 i=0 · 加入第 0 个:起点固定在 0,右端扩到第 0 个 1。这是窗口里的新面孔,不同元素数涨到 1。
起点 i=0 · 判定:窗口不同元素才 1 种,离 k = 3 还差 2 种,这段不算完全子数组,继续把右端往右扩。
起点 i=0 · 加入第 1 个:起点固定在 0,右端扩到第 1 个 3。这是窗口里的新面孔,不同元素数涨到 2。
起点 i=0 · 判定:窗口不同元素才 2 种,离 k = 3 还差 1 种,这段不算完全子数组,继续把右端往右扩。
起点 i=0 · 加入第 2 个:起点固定在 0,右端扩到第 2 个 1。它之前已经在窗口里出现过,集合不变,不同元素数还是 2。
起点 i=0 · 判定:窗口不同元素才 2 种,离 k = 3 还差 1 种,这段不算完全子数组,继续把右端往右扩。
起点 i=0 · 加入第 3 个:起点固定在 0,右端扩到第 3 个 2。这是窗口里的新面孔,不同元素数涨到 3。
起点 i=0 · 判定:窗口不同元素 3 恰好等于 k = 3,[0, 3] 凑齐了全部 3 种,是一个完全子数组,ans 变成 1。
起点 i=0 · 加入第 4 个:起点固定在 0,右端扩到第 4 个 2。它之前已经在窗口里出现过,集合不变,不同元素数还是 3。
起点 i=0 · 判定:窗口不同元素 3 恰好等于 k = 3,[0, 4] 凑齐了全部 3 种,是一个完全子数组,ans 变成 2。
起点 i=1 · 加入第 1 个:起点固定在 1,右端扩到第 1 个 3。这是窗口里的新面孔,不同元素数涨到 1。
起点 i=1 · 判定:窗口不同元素才 1 种,离 k = 3 还差 2 种,这段不算完全子数组,继续把右端往右扩。
起点 i=1 · 加入第 2 个:起点固定在 1,右端扩到第 2 个 1。这是窗口里的新面孔,不同元素数涨到 2。
起点 i=1 · 判定:窗口不同元素才 2 种,离 k = 3 还差 1 种,这段不算完全子数组,继续把右端往右扩。
起点 i=1 · 加入第 3 个:起点固定在 1,右端扩到第 3 个 2。这是窗口里的新面孔,不同元素数涨到 3。
起点 i=1 · 判定:窗口不同元素 3 恰好等于 k = 3,[1, 3] 凑齐了全部 3 种,是一个完全子数组,ans 变成 3。
起点 i=1 · 加入第 4 个:起点固定在 1,右端扩到第 4 个 2。它之前已经在窗口里出现过,集合不变,不同元素数还是 3。
起点 i=1 · 判定:窗口不同元素 3 恰好等于 k = 3,[1, 4] 凑齐了全部 3 种,是一个完全子数组,ans 变成 4。
起点 i=2 · 第 2 个:起点 2 从第 2 个数到第 2 个,不同元素最多才 1 种,凑不齐 3 种。起点 2 往后再怎么扩也只有这些元素,这一轮数不出完全子数组。
起点 i=2 · 第 3 个:起点 2 从第 2 个数到第 3 个,不同元素最多才 2 种,凑不齐 3 种。起点 2 往后再怎么扩也只有这些元素,这一轮数不出完全子数组。
起点 i=2 · 第 4 个:起点 2 从第 2 个数到第 4 个,不同元素最多才 2 种,凑不齐 3 种。起点 2 往后再怎么扩也只有这些元素,这一轮数不出完全子数组。
起点 i=3 · 第 3 个:起点 3 从第 3 个数到第 3 个,不同元素最多才 1 种,凑不齐 3 种。起点 3 往后再怎么扩也只有这些元素,这一轮数不出完全子数组。
起点 i=3 · 第 4 个:起点 3 从第 3 个数到第 4 个,不同元素最多才 1 种,凑不齐 3 种。起点 3 往后再怎么扩也只有这些元素,这一轮数不出完全子数组。
起点 i=4 · 第 4 个:起点 4 从第 4 个数到第 4 个,不同元素最多才 1 种,凑不齐 3 种。起点 4 往后再怎么扩也只有这些元素,这一轮数不出完全子数组。
收束 · 汇总答案:五个起点都枚举完了。起点 0 数到 [0,3]、[0,4] 两个,起点 1 数到 [1,3]、[1,4] 两个,后面三个起点凑不齐 3 种。加起来完全子数组共 4 个。
边界先想清:全相同、全不同、单元素。全相同时每段都完全,答案是子数组总数。
面试重点:认出「固定右端、维护左端边界、按左端可选数累加」的滑动窗口计数母题,能把 O(n²) 压到 O(n)。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def countCompleteSubarrays(self, nums: List[int]) -> int: cnt = len(set(nums)) ans, n = 0, len(nums) for i in range(n): s = set() for x in nums[i:]: s.add(x) if len(s) == cnt: ans += 1 return ans复杂度
- 时间:O(n²),外层枚举起点、内层枚举右端,集合增删查平均 O(1)
- 空间:O(n),集合最多装下 n 个不同元素;Python 切片 nums[i:] 另有 O(n) 拷贝但不改量级
易错点
面试追问把动画讲成自己的话
追问这个 O(n²) 还能优化吗?
追问它和「至多 K 个不同整数的子数组」是一类题吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
使二进制数组全部等于 1 的最少操作次数 I
LeetCode 3191 · 中等 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题