左神做法的理论依据
我们可以通过 集合的包含关系 和 具体示例枚举 来直观理解这一推导过程。以下结合题目示例 1 进行详细说明:
示例 1 分析
输入 :nums = [1,2,1,2,3]
, k = 2
目标 :计算恰好包含 2 种不同整数 的子数组个数。
步骤一集合 A 的计数
滑动窗口的核心逻辑是:
- 对于每个右边界
r
,找到最小左边界l
,使得窗口[l, r]
内不同整数个数 ≤k。 - 此时,左边界可以是
[l, r]
中的任意位置,因此贡献r - l + 1
个子数组。
以示例 1 为例,滑动窗口计算 findNumsofMost(nums, 2)
(即 |A|)的过程如下:
r | 元素 | 窗口内元素 | 不同数 | 最小左边界 l | 贡献子数组数 |
---|---|---|---|---|---|
0 | 1 | [1] | 1 | 0 | 1 |
1 | 2 | [1,2] | 2 | 0 | 2 |
2 | 1 | [1,2,1] | 2 | 0 | 3 |
3 | 2 | [1,2,1,2] | 2 | 0 | 4 |
4 | 3 | [2,1,2,3] | 3(超过 2) | 移动 l 到 1 | 窗口 [1,4] 含 2,1,3(3 种,仍超过)→ 继续移动 l 到 2 → 窗口 [2,4] 含 1,2,3(3 种,仍超过)→ 移动 l 到 3 → 窗口 [3,4] 含 2,3(2 种)→ 最小 l=3,贡献 (4-3+1=2) |
累加结果 :(1+2+3+4+2=12),即 |A|=12。
而 findNumsofMost(nums, 1)
(即 |B|)的计算结果为 5(仅含单个元素的子数组),因此:
步骤 2:定义集合 B(不超过 k-1 种)
集合 B 包含所有不同整数个数 ≤1 的子数组(即仅含 1 种整数)。
枚举所有满足条件的子数组:
- 单个元素:
[1], [2], [1], [2], [3]
,共 5 个。 - 连续相同元素的子数组:
[1]
(位置 0)、[2]
(位置 1)、[1]
(位置 2)、[2]
(位置 3)、[3]
(位置 4)。- 无长度 ≥2 的连续相同元素(因为数组中相邻元素不同)。
- 集合 B 的总数:5 个。
步骤 3:计算集合差集 A - B
根据定义:
恰好含 k 种的子数组个数 = |A| - |B|
-
集合 A 包含 含 1 种或 2 种 的子数组。
-
集合 B 包含 仅含 1 种 的子数组。
-
因此,A - B 即为仅含 2 种的子数组 ,其数量为:
|A| - |B| = 12 - 5 = 7
这与题目示例 1 的输出一致。
数学推导:集合差集的严格性
-
设 (f(m)) 表示 不同整数个数 ≤m 的子数组个数。
-
恰好含 k 种的子数组必须满足:
- 不同整数个数 ≤k(属于集合 A),
- 且不同整数个数 >k-1(不属于集合 B)。
-
因此,恰好含 k 种的子数组是 A 中排除 B 的部分,即:
= f(k) - f(k-1)
例子 2:更简单的数组(nums = [1,1,2], k=2)
为了更直观,我们用一个更短的数组验证。
问题目标
找到所有「恰好有 2 种不同整数」的子数组。
数组分析
数组 [1,1,2]
的所有子数组及其不同整数个数:
子数组 | 内容 | 不同整数个数 |
---|---|---|
[0,0] | [1] | 1 |
[0,1] | [1,1] | 1 |
[0,2] | [1,1,2] | 2 |
[1,1] | [1] | 1 |
[1,2] | [1,2] | 2 |
[2,2] | [2] | 1 |
集合 A(≤2)和 B(≤1)的大小
- 集合 A(≤2):所有子数组都满足(因为最大不同个数是 2),共 6 个。
- 集合 B(≤1) :不同整数个数为 1 的子数组,即
[0,0], [0,1], [1,1], [2,2]
→ 共 4 个。
结论验证
恰好有 2 种不同整数的子数组是 [0,2], [1,2]
→ 共 2 个。
根据公式 |A| - |B| = 6 - 4 = 2
,与实际结果一致。
数学推导:为什么差集是正确的?
用数学符号形式化定义:
- 设
f(k)
为「不同整数个数 ≤k 的子数组数目」。 - 设
g(k)
为「不同整数个数恰好等于k 的子数组数目」。
根据定义,f(k)
是所有 g(1), g(2), ..., g(k)
的和,即:
f(k) = g(1) + g(2) + ... + g(k)
同理,f(k-1)
是:
f(k-1) = g(1) + g(2) + ... + g(k-1)
两式相减得:
f(k) - f(k-1) = g(k)
这正是题目要求的「恰好有k种不同整数的子数组数目」。
总结
通过具体例子和数学推导可以看出:
「不超过k的子数组数」减去「不超过k-1的子数组数」,本质是通过「前缀和相减」的方式,精准提取出「恰好等于k」的子数组数目。这种方法将复杂的「恰好k」问题转化为更易计算的「不超过k」问题,是滑动窗口算法中常用的技巧。
代码
java
class Solution {
public int subarraysWithKDistinct(int[] nums, int k) {
return findNumsofMost(nums, k) - findNumsofMost(nums, k - 1);
}
private static final int maxn = 20001;
private static int[] cnts = new int[maxn];
private int findNumsofMost(int[] nums, int k) {
Arrays.fill(cnts, 1, nums.length + 1, 0);
int ans = 0;
for (int r = 0, l = 0, collect = 0; r < nums.length; r ++) {
if (++cnts[nums[r]] == 1) {
collect ++;
}
while (collect > k) {
if (--cnts[nums[l++]] == 0) {
collect--;
}
}
ans += r - l + 1;
}
return ans;
}
}