以下为力扣的官方题解
题目
给定一个正整数数组 A A A,如果 A A A 的某个子数组中不同整数的个数恰好为 K K K,则称 A A A 的这个连续、不一定独立的子数组为好子数组。
(例如, [ 1 , 2 , 3 , 1 , 2 ] [1,2,3,1,2] [1,2,3,1,2] 中有 3 3 3 个不同的整数: 1 1 1, 2 2 2,以及 3 3 3。)
返回 A A A 中好子数组的数目。
示例1
输入: A = [ 1 , 2 , 1 , 2 , 3 ] , K = 2 A = [1,2,1,2,3], K = 2 A=[1,2,1,2,3],K=2
输出: 7 7 7
解释:恰好由 2 2 2 个不同整数组成的子数组: [ 1 , 2 ] , [ 2 , 1 ] , [ 1 , 2 ] , [ 2 , 3 ] , [ 1 , 2 , 1 ] , [ 2 , 1 , 2 ] , [ 1 , 2 , 1 , 2 ] [1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2] [1,2],[2,1],[1,2],[2,3],[1,2,1],[2,1,2],[1,2,1,2]
示例2
输入: A = [ 1 , 2 , 1 , 3 , 4 ] , K = 3 A = [1,2,1,3,4], K = 3 A=[1,2,1,3,4],K=3
输出: 3 3 3
解释:恰好由 3 3 3 个不同整数组成的子数组: [ 1 , 2 , 1 , 3 ] , [ 2 , 1 , 3 ] , [ 1 , 3 , 4 ] [1,2,1,3], [2,1,3], [1,3,4] [1,2,1,3],[2,1,3],[1,3,4]
提示
- 1 < = A . l e n g t h < = 20000 1 <= A.length <= 20000 1<=A.length<=20000
- 1 < = A [ i ] < = A . l e n g t h 1 <= A[i] <= A.length 1<=A[i]<=A.length
- 1 < = K < = A . l e n g t h 1 <= K <= A.length 1<=K<=A.length
官方题解 滑动窗口
我们容易发现,对于任意一个右端点,可能存在一系列左端点与其对应,满足两端点所指区间对应的子数组内恰有 K K K 个不同整数。因此可能有 O ( n 2 ) O(n^2) O(n2) 个子数组满足条件。因此无法暴力解决该题。
分析这些左端点,我们可以证明:对于任意一个右端点,能够与其对应的左端点们必然相邻。
证明非常直观,假设区间 [ l 1 , r ] [l1,r] [l1,r] 和 [ l 2 , r ] [l2,r] [l2,r] 为满足条件的数组(不失一般性,设 l 1 ≤ l 2 l1 \leq l2 l1≤l2)。现在我们设存在一个 l l l 满足 l 1 ≤ l ≤ l 2 l1 \leq l \leq l2 l1≤l≤l2,那么区间 [ l , r ] [l,r] [l,r] 作为 [ l 1 , r ] [l1,r] [l1,r] 的子数组,其中的不同整数数量必然不超过 K K K。同理,区间 [ l , r ] [l,r] [l,r] 作为 [ l 2 , r ] [l2,r] [l2,r] 的父数组,其中的不同整数数量必然不少于 K K K。那么可知区间 [ l , r ] [l,r] [l,r] 中的不同整数数量即为 K K K。
这样我们就可以用一个区间 [ l 1 , l 2 ] [l1,l2] [l1,l2] 来代表能够与右端点 r r r 对应的左端点们。
同时,我们可以发现:当右端点向右移动时,左端点区间也同样向右移动。因为当我们在原有区间的右侧添加元素时,区间中的不同整数数量不会减少而只会不变或增加,因此我们需要在区间左侧删除一定元素,以保证区间内整数数量仍然为 K K K。
于是我们可以用滑动窗口解决本题,和普通的滑动窗口解法的不同之处在于,我们需要记录两个左指针 l e f t 1 left1 left1 与 l e f t 2 left2 left2 来表示左端点区间 [ l e f t 1 , l e f t 2 ) [left1,left2) [left1,left2)。第一个左指针表示极大的包含 K K K 个不同整数的区间的左端点,第二个左指针则表示极大的包含 K − 1 K-1 K−1 个不同整数的区间的左端点。
代码(含注释)
class Solution {
public int subarraysWithKDistinct(int[] A, int K) {
int n = A.length;
int[] num1 = new int[n+1]; // 统计滑动窗口中各个数字出现的次数
int[] num2 = new int[n+1];
int tot1 = 0, tot2 = 0; // 统计滑动窗口中有几个不同的数字
int left1 = 0, left2 = 0, right = 0;
int ret = 0;
while (right < n)
{
if (num1[A[right]] == 0) // 如果数字是第一次出现,则种类加 1
tot1 ++;
num1[A[right]] ++;
if (num2[A[right]] == 0)
tot2 ++;
num2[A[right]] ++;
while (tot1 > K) // 如果数字种类超过了 K,则把左指针左移
{
num1[A[left1]] --;
if (num1[A[left1]] == 0) // 如果移除数字之后,窗口中没有该数字,则把数字种类减 1
tot1 --;
left1 ++;
}
while (tot2 > K-1)
{
num2[A[left2]] --;
if (num2[A[left2]] == 0)
tot2 --;
left2 ++;
}
// ret += (right-left1) - (right-left2);
ret += left2 - left1;
right ++;
}
return ret;
}
}
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组长度。我们至多只需要遍历该数组三次(右指针和两个左指针各一次)。
- 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组长度。我们需要记录每一个数的出现次数,本题中数的大小不超过数组长度。