分治:至少有K个重复字符的最长子串

Posted KuoGavin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分治:至少有K个重复字符的最长子串相关的知识,希望对你有一定的参考价值。

  分治策略的常见应用有二分法(分治应用于边界划分)归并排序快速排序,实现和详解见排序归纳总结(插入排序、归并排序、堆排序、快速排序、桶排序)

  在此简单总结一下分治的思想,一个问题可以拆分成众多的相同结构的子问题的求解,且子问题和原问题是相同性质的,且分出的子问题之间互不影响也即相互独立。同时,某个子问题中的解就是原问题的解。


接下来进入正题,以 395. 至少有K个重复字符的最长子串 为例,再理一下分治的思路。还有一道题,要求 O ( n l o g n ) O(nlogn) O(nlogn)时间复杂度, O ( 1 ) O(1) O(1)空间复杂度重排链表为有序链表:148. 排序链表,其实就是数归并排序的链表实现,这里就展开了。

题目

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

示例 1:
输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。

示例 2:
输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。

思路

一、分治法

  对于该问题,通过观察不难发现字符串中出现频次少于所要求频次k的字符,一定不会出现在所求的最长子串当中,则利用这一性质可以对整个字符串进行分割,分割为左右两个子串,再分别求解左右子串的最长子串,在所有符合题设的最长子串当中找出长度最长的子串并返回其长度即可完成题目要求。由此可以确立能够使用分治方法进行解题。

具体思路为:
  先将整个字符串遍历一编,统计每个字符出现的频次从头开始遍历字符串,若是发现当前字符所出现频次小于要求的频次,则说明再所求的最长子串当中不会包含这个字符,则可将整个字符串分解为求解当前所遍历的字符为分割点的左子串和右子串的最长子串中的最大值。举例:

对于一个字符串来说,如果要求子串最少出现k次,那么如果某些字母出现的次数小于k,
这些字母一定不会出现在最长的子串中,并且这些字母将整个字符子串分割成小段,这些小段有可能是最长的
但是由于被分割了,还是要检查这一小段,如果某些字母出现的次数小于k,会将小段继续分割下去,
比如字符串"aacbbbdc",要求最少出现2次,我们记录左右闭区间,,
第一轮[0,7],处理"aacbbbdc",d只出现了一次不满足,于是递归解决区间[0,5]、[7,7]
第二轮[0,5],处理"aacbbb",  c只出现了一次不满足,于是递归解决区间[0,1]、[3,4] 
第二轮[7,7],处理"c",       c只出现了一次不满足,不继续递归
第三轮[0,1],处理"aa",      满足出现次数>=2,ret=2
第三轮[3,4],处理"bbb",     满足出现次数>=2 ret=3;

  注意代码实现中,通常分治和递归是伴生的,因为将大问题拆分为同等子问题,得到子问题的答案并更新大问题答案就意味着递归调用的产生。牵扯到递归,有三大要素,分别是返回条件本层递归中的行为返回值

  对于返回条件和返回值:①如果k不大于2,则可直接返回s的长度;②若是s为空,或s长度小于k直接返回0;③若是整个字符串中的字符出现频次都大于等于k,则直接返回字符串的长度;④否则返回分割后的左右子串的最长子串长度中较大值;

  对于本层中的递归动作,就是找出分割点/区间,进行递归调用。

    int longestSubstring(string s, int k) 
        if(k <= 1) return s.size();
        if(s.empty() || s.size() < k) return 0;

        int hash[26] = 0;
        for(auto ch : s) ++hash[ch - 'a'];

        int i = 0;
        while(i < s.size() && hash[s[i] - 'a'] >= k) ++i;
        if(i == s.size()) return s.size();

        int l = longestSubstring(s.substr(0, i), k);
        //优化操作,将分割点进行拓展,找到分割子串,一直将i遍历至出现频次>=k的字符位置
        while(i < s.size() && hash[s[i] - 'a'] < k) i++;
        //i = i == s.size() - 1 ? i : i + 1;
        int r = longestSubstring(s.substr(i, s.size()), k);

        return max(l, r);
    
时间复杂度O(n * ∣Σ∣), 空间复杂度O(∣Σ∣ * ∣Σ∣),其中n是字符串长度,∣Σ∣最大递归深度。

二、滑动窗口

作为扩展,也是一种方法,并不直观。

对于符合题目要求的子串,其中包含的字符的类型限制在了小写字母这一字符集上,大小为∣Σ∣ = 26
假设满足题设的最长的子串包含的字符类型数为type,维护一滑动窗口,记录其中该窗口中的如下信息;
1.左边界索引l;
2.右边界索引r;
3.窗口中每种字符的出现次数hash[s[idx] - 'a'];
4.窗口中出现次数小于所要求K的字符种类数lessK

有了如上窗口信息之后,则维护滑动窗口的思路便有了,目标就是维护滑动窗口中的字符种类数等于type
在维护的过程中,整体是右移窗口的右边界l(时间复杂度O(n))):
1.若是窗口中种类没达到type则继续右移右边界l,更新窗口信息
2.若是发现窗口中种类数大于type,则此时需要右移左边界r,同时更新窗口信息
3.若是发现窗口中种类数等于type,且lessK等于0,则此时的滑动窗口是满足题设的子串,更新最长子串长度即可
    int longestSubstring(string s, int k) 
        int res = 0;
        for(int types = 1; types <= 26; ++types) 
            int l = 0, r = 0;
            int hash[26] = 0;
            int type = 0, lessK = 0;
            while(r < s.size()) 
                hash[s[r] - 'a']++;
                if(hash[s[r] - 'a'] == 1)  type++; lessK++;
                if(hash[s[r] - 'a'] == k) lessK--;

                while(type > types) 
                    hash[s[l] - 'a']--;
                    if(hash[s[l] - 'a'] == k - 1) lessK++;
                    if(hash[s[l] - 'a'] == 0)  type--; lessK--;
                    l++;
                
                if(lessK == 0) res = max(res, r - l + 1);
                r++;
            
        
        return res;
    
时间复杂度为O(∣Σ∣*(n + ∣Σ∣)),空间复杂度为O(∣Σ∣)

以上是关于分治:至少有K个重复字符的最长子串的主要内容,如果未能解决你的问题,请参考以下文章

字符串类题目——滑动窗口和递归分治以及一些些位运算的结合

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

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

题目地址(395. 至少有 K 个重复字符的最长子串)

题目地址(395. 至少有 K 个重复字符的最长子串)

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