【力扣】395. 至少有K个重复字符的最长子串

以下为力扣官方题解

395. 至少有K个重复字符的最长子串

  • 题目
  • 示例1
  • 示例2
  • 提示
  • 官方题解
    • 方法一:分治
      • 代码
      • 复杂度分析
    • 方法二:滑动窗口
      • 代码
      • 复杂度分析

题目

给你一个字符串 s s s 和一个整数 k k k,请你找出 s s s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k k k 。返回这一子串的长度。

示例1

输入 s = " a a a b b " , k = 3 s = "aaabb", k = 3 s="aaabb",k=3
输出 3 3 3
解释:最长子串为 “ a a a aaa aaa” ,其中 ‘ a a a’ 重复了 3 3 3 次。

示例2

输入 s = " a b a b b c " , k = 2 s = "ababbc", k = 2 s="ababbc",k=2
输出 5 5 5
解释:最长子串为 “ a b a b b ababb ababb” ,其中 ‘ a a a’ 重复了 2 2 2 次, ‘ b b b’ 重复了 3 3 3 次。

提示

  1. 1 < = s . l e n g t h < = 1 0 4 1 <= s.length <= 10^4 1<=s.length<=104
  2. s s s 仅由小写英文字母组成
  3. 1 < = k < = 1 0 5 1 <= k <= 10^5 1<=k<=105

官方题解

方法一:分治

对于字符串 s s s,如果存在某个字符 c h ch ch,它的出现次数大于 0 0 0 且小于 k k k,则任何包含 c h ch ch 的子串都不可能满足要求。也就是说,我们将字符串按照 c h ch ch 切分成若干段,则满足要求的最长子串一定出现在某个被切分的段内,而不能跨越一个或多个段。因此,可以考虑分治的方式求解本题。

代码

class Solution {
    public int longestSubstring(String s, int k) {
        int n = s.length();
        return dfs(s, 0, n-1, k);
    }

    public int dfs(String s, int l, int r, int k) {
        int[] cnt = new int[26];
        for (int i=l; i<=r; i++)
        {
            cnt[s.charAt(i)-'a'] ++;
        }

        char split = 0;
        for (int i=0; i<26; i++)
        {
            if (cnt[i]>0 && cnt[i]<k)
            {
                split = (char) (i+'a');
                break;
            }
        }
        if (split == 0)
        {
            return r-l+1;
        }

        int i = l;
        int ret = 0;
        while (i <= r)
        {
            while (i<=r && s.charAt(i)==split)
            {
                i ++;
            }
            if (i > r)
            {
                break;
            }
            int start = i;
            while (i<=r && s.charAt(i)!=split)
            {
                i ++;
            }

            int length = dfs(s, start, i-1, k);
            ret = Math.max(ret, length);
        }
        return ret;
    }
}

复杂度分析

  • 时间复杂度: O ( N ⋅ ∣ Σ ∣ ) O(N\cdot |\Sigma|) O(NΣ),其中 N N N 为字符串的长度, Σ \Sigma Σ 为字符集,本题中字符串仅包含小写字母,因此 ∣ Σ ∣ = 26 |\Sigma| = 26 Σ=26。由于每次递归调用都会完全去除某个字符,因此递归深度最多为 ∣ Σ ∣ |\Sigma| Σ
  • 空间复杂度: O ( ∣ Σ ∣ 2 ) O(|\Sigma|^2) O(Σ2)。递归的深度为 O ( ∣ Σ ∣ ) O(|\Sigma|) O(Σ),每层递归需要开辟 O ( ∣ Σ ∣ ) O(|\Sigma|) O(Σ) 的额外空间。

方法二:滑动窗口

我们枚举最长子串中的字符种类数目,它最小为 1 1 1,最大为 ∣ Σ ∣ |\Sigma| Σ(字符集的大小,本题中为 26 26 26)。

对于给定的字符种类数量 t t t,我们维护滑动窗口的左右边界 l , r l,r l,r、滑动窗口内部每个字符出现的次数 c n t cnt cnt,以及滑动窗口内的字符种类数目 t o t a l total total。当 t o t a l > t total>t total>t 时,我们不断地右移左边界 l l l,并对应地更新 c n t cnt cnt 以及 t o t a l total total,直到 total ≤ t \textit{total} \le t totalt 为止。这样,对于任何一个右边界 r r r,我们都能找到最小的 l l l(记为 l m i n l_{min} lmin),使得 s [ l m i n . . . r ] s[l_{min}...r] s[lmin...r] 之间的字符种类数目不多于 t t t

对于任何一组 l m i n , r l_{min}, r lmin,r 而言,如果 s [ l m i n . . . r ] s[l_{min}...r] s[lmin...r] 之间存在某个出现次数小于 k k k (且不为 0 0 0,下文不再特殊说明)的字符,我们可以断定:对于任何 l ′ ∈ ( l m i n , r ) l' \in (l_{min}, r) l(lmin,r) 而言, s [ l ′ . . . r ] s[l'...r] s[l...r] 依然不可能是满足题意的子串,因为:

  • 要么该字符的出现次数降为 0 0 0,此时子串内虽然少了一个出现次数小于 k k k 的字符,但字符种类数目也随之小于 t t t 了;
  • 要么该字符的出现次数降为非 0 0 0 整数,此时该字符的出现次数依然小于 k k k

根据上面的结论,我们发现:当限定字符种类数目为 t t t 时,满足题意的最长子串,就一定出自某个 s [ l m i n . . . r ] s[l_{min}...r] s[lmin...r]。因此,在滑动窗口的维护过程中,就可以直接得到最长子串的大小。

此外还剩下一个细节:如何判断某个子串内的字符是否都出现了至少 k k k 次?我们当然可以每次遍历 c n t cnt cnt 数组,但是这会带来 O ( ∣ Σ ∣ ) O(|\Sigma|) O(Σ) 的额外开销。

我们可以维护一个计数器 l e s s less less,代表当前出现次数小于 k k k 的字符的数量。注意到:每次移动滑动窗口的边界时,只会让某个字符的出现次数加一或者减一。对于移动右边界 l l l 的情况而言:

  • 当某个字符的出现次数从 0 0 0 增加到 1 1 1 时,将 l e s s less less 加一;
  • 当某个字符的出现次数从 k − 1 k-1 k1 增加到 k k k 时,将 l e s s less less 减一。

对于移动左边界的情形,讨论是类似的。

通过维护额外的计数器 l e s s less less,我们无需遍历 c n t cnt cnt 数组,就能知道每个字符是否都出现了至少 k k k 次,同时可以在每次循环时,在常数时间内更新计数器的取值。读者可以自行思考 k = 1 k=1 k=1 时的处理逻辑。

代码

class Solution {
    public int longestSubstring(String s, int k) {
        int ret = 0; 
        int n = s.length();
        for (int t=1; t<=26; t++)
        {
            int l = 0, r = 0;
            int[] cnt = new int[26];
            int tot = 0;
            int less = 0;
            while (r < n)
            {
                cnt[s.charAt(r)-'a'] ++;
                if (cnt[s.charAt(r)-'a'] == 1)
                {
                    tot ++;
                    less ++;
                }
                if (cnt[s.charAt(r)-'a'] == k)
                {
                    less --;
                }

                while (tot > t)
                {
                    cnt[s.charAt(l)-'a'] --;
                    if (cnt[s.charAt(l)-'a'] == k-1)
                    {
                        less ++;
                    }
                    if (cnt[s.charAt(l)-'a'] == 0)
                    {
                        tot --;
                        less --;
                    }
                    l ++;
                }
                if (less == 0)
                {
                    ret = Math.max(ret, r-l+1);
                }
                r ++;
            }
        }
        return ret;
    }
}

复杂度分析

  • 时间复杂度: O ( N ⋅ ∣ Σ ∣ + ∣ Σ ∣ 2 ) O(N \cdot |\Sigma| + |\Sigma|^2) O(NΣ+Σ2),其中 N N N 为字符串的长度, Σ \Sigma Σ 为字符集,本题中字符串仅包含小写字母,因此 ∣ Σ ∣ = 26 |\Sigma| = 26 Σ=26。我们需要遍历所有可能的 t t t,共 ∣ Σ ∣ |\Sigma| Σ 种可能性;内层循环中滑动窗口的复杂度为 O ( N ) O(N) O(N),且初始时需要 O ( ∣ Σ ∣ ) O(|\Sigma|) O(Σ) 的时间初始化 c n t cnt cnt 数组。
  • 空间复杂度: O ( ∣ Σ ∣ ) O(|\Sigma|) O(Σ)

你可能感兴趣的:(力扣,java,leetcode)