【力扣】992. K 个不同整数的子数组

以下为力扣的官方题解

题目

给定一个正整数数组 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. 1 < = A . l e n g t h < = 20000 1 <= A.length <= 20000 1<=A.length<=20000
  2. 1 < = A [ i ] < = A . l e n g t h 1 <= A[i] <= A.length 1<=A[i]<=A.length
  3. 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 l1l2)。现在我们设存在一个 l l l 满足 l 1 ≤ l ≤ l 2 l1 \leq l \leq l2 l1ll2​,那么区间 [ 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 K1 个不同整数的区间的左端点。

代码(含注释)

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 是数组长度。我们需要记录每一个数的出现次数,本题中数的大小不超过数组长度。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值