leetcode第291场周赛总结复盘


前言

经过将近一年的刷题和总结学习,感觉算法的基础部分算是啃下来了,但是还没有吃透,模拟、滑动窗口、集合的应用,动态规划、dfs…但是还是有很多还没有学习和掌握的,例如:图、并查集、bfs等等。怀揣着激动的心尝试了自己的第一次周赛,结果有些不尽人意,特作此复盘,希望自己和各位一样能不断学习和继续进步叭~


题目与题解

先分享一下自己的题目与题解,有更好的题解方法也可以在评论区分享一起学习!

1.移除指定数字得到最大结果(Easy)

题目链接

题目:

给你一个表示某个正整数的字符串 number 和一个字符 digit 。
从 number 中 恰好 移除 一个 等于 digit 的字符后,找出并返回按 十进制 表示 最大 的结果字符串。生成的测试用例满足 digit 在 number 中出现至少一次。

在这里插入图片描述

思路

其实第一题是简单题,按道理来说应该没那么复杂,所以我一上来就是将遍历字符串,遇到符合条件的digit时就删除这个字符然后将其转换成Integer进行比较

错误代码1
class Solution {
    public String removeDigit(String number, char digit) {
        int result=0;
        int n=number.length();
        for(int i=0;i<n;i++){
            if(number.charAt(i)==digit){
                //如果当前字符与所示字符相同,那么将其移除
                String str=number.substring(0,i)+number.substring(i+1,n);
                int temp=Integer.valueOf(str);
                result=max(result,temp);
            }
        }
        return String.valueOf(result);
    }
    
    public int max(int a,int b){
        return a>b?a:b;
    }
}
运行结果1

在这里插入图片描述
仔细看题之后发现整个字符串可以长达100位,我使用Integer会不会是远远不够了hh,所以我必须转变思路,使用字符串进行比较

于是我转变思路:能不能通过字符串的一些规律来解决此题
对于任意一个字符串str,我发现遇到的第一个匹配字符是相当重要的,例如:

示例1:
str=“3213”,digit=‘3’

去掉第一个3会使结果变成213,去掉第2个3会让结果变成321(优)

示例2:
str=“3413”,digit=‘3’

去掉第1个3会使结果变成413(优),去掉第2个3会让结果变成341

示例3:
str=“3313”,digit=‘3’

去掉第1个3或第2个3都是313,去掉最后一个3会使结果位331(优)

示例4:
str=“44443313”,digit=‘3’

如示例3,前面的数对其没有影响

因此得出结论:如果当前子串的数字为digit,那么需要结合下一位进行判断,如果下一位大于digit,那么就直接删除这个数然后返回结果即可(埋坑)

错误代码2
class Solution {
    public String removeDigit(String number, char digit) {
        int n=number.length();
        String result="";
        int nums=0;
        /****
            第一轮找是否只有一个字符
        **/
        for(int i=0;i<n;i++){
            if(number.charAt(i)==digit){
                nums++;
            }
        }
        if(nums==1){
            for(int i=0;i<n;i++){
                if(number.charAt(i)==digit){
                    result=number.substring(0,i)+number.substring(i+1,n);
                    break;
                }
            }
        }else{
            //至少有两个符合条件的字符
            for(int i=0;i<n;i++){
                if(number.charAt(i)==digit){
                    if(i==n-1){
                        result=number.substring(0,n-1);
                        break;
                    }
                    if(number.charAt(i+1)>number.charAt(i)){
                        result=number.substring(0,i)+number.substring(i+1,n);
                        break;
                    }
                }
            }
        }
        
        return result;
    }
}

运行返回的结果是:“”(空子串)

分析:
为什么返回的是空子串呢,因为到最后一个5时还没有符合条件,所以整个字符串跳过去后都进行操作,也就没有结果返回了,所以我还需要记录这是不是操作的最后一个数

正确代码
class Solution {
    public String removeDigit(String number, char digit) {
        int n=number.length();
        String result="";
        int nums=0;
        /****
            第一轮找是否只有一个字符
        **/
        for(int i=0;i<n;i++){
            if(number.charAt(i)==digit){
                nums++;
            }
        }
        if(nums==1){
            for(int i=0;i<n;i++){
                if(number.charAt(i)==digit){
                    result=number.substring(0,i)+number.substring(i+1,n);
                    break;
                }
            }
        }else{
            //至少有两个符合条件的字符
            for(int i=0;i<n;i++){
                if(number.charAt(i)==digit){
                    nums--;     //表示后面还有几个数
                    if(i==n-1||nums==0){
                        result=number.substring(0,i)+number.substring(i+1,n);
                        break;
                    }
                    if(number.charAt(i+1)>number.charAt(i)){
                        result=number.substring(0,i)+number.substring(i+1,n);
                        break;
                    }
                }
            }
        }
        
        return result;
    }
}

在这里插入图片描述
第一题做成这样实属不应该,罚时了20分钟加上思考了30分钟,也就成为了拖后腿的一题.

2.必须拿起的最小连续卡牌数

题目链接

题目

在这里插入图片描述

思路

这题我一开始是想用哈希表来存储每个数字出现的位置的,然后再遍历哈希表来计算每个key中的value差值最小值,后来一想其实不需要那么复杂,cards[i]在[0,106]之间,使用数组来实现哈希表或许会更加简单,思路清晰后也很快编写出对应的代码:

正确代码
class Solution {
    public int minimumCardPickup(int[] cards) {
        /****
            使用数组来存储对应数和对应位置
        **/
        int n = cards.length;
        int[] idx = new int[1000001];
        for(int i=0;i<idx.length;i++){
            idx[i]=-1;
        }
        int result=1000001;
        for(int i=0;i<n;i++){
            if(idx[cards[i]]!=-1){
                //说明之前这个数之前出现过,那么找出上一个出现该数的位置
                int subIndex=i-idx[cards[i]]+1;
                result=Math.min(result,subIndex);
            }
            idx[cards[i]]=i;
        }
        
        return result==1000001?-1:result;
    }
}

运行结果
在这里插入图片描述

3.含最多k个可整除元素的子数组

题目链接

题目

在这里插入图片描述

思路

其实这题也并不复杂,像是作一个符合条件的全排列,问题就在于我对于Set集合和List集合理解不够深入,导致测试时本题卡了好久,提交本题时已经结束10分钟了,自然分数也就没有拿到,有点可惜吧

上文提到这里可以使用Set实现去重,不断比较是否可以添加进新的元素进List,然后将List放入Set中实现去重,于是第一版的错误代码便应运而生了…

错误代码1
class Solution {
    /****
     全排列问题,使用Set去重会更佳
     ***/
    public int countDistinct(int[] nums, int k, int p) {
        int n = nums.length;
        Set<List<Integer>> set=new HashSet<>();
        for(int i=0;i<n;i++){
            List<Integer> list = new ArrayList<>();
            int temp = k;
            for(int j=i;j<n;j++){
                if(nums[j]%p==0){
                    if(temp==0){
                        break;
                    }
                    temp--;
                }
                list.add(nums[j]);
                set.add(list);
            }
        }

        return set.size();
    }
}

错误结果:

填入示例:
nums={2, 3, 3, 2, 2},k=2,p=2后结果为14,而不是正确结果11,为此我进行调试,调试时发现很多"诡异"之处:

第一次进入内层循环时:set中的list有1个,为{2}
在这里插入图片描述
第二次进入内层循环时:set中的list有两个,且它们都是{2,3}
在这里插入图片描述
由于第一次调试时心情急躁,也就没有认真仔细一步步调试,而是到最后才发现竟然有许多重复的集合,这让我一时间很无解,后来我才发现我操作的都是同一条链表,即对同一引用进行了重复的操作,原来的链表也会随着add操作而继续改变,想清楚这点时,比赛已经结束了,但是我还是提交了代码,结果真就差了亿点…

正确代码
class Solution {
    /****
     全排列问题,使用Set去重会更佳
     ***/
    public int countDistinct(int[] nums, int k, int p) {
        int n = nums.length;
        Set<List<Integer>> set=new HashSet<>();
        for(int i=0;i<n;i++){
            List<Integer> list = new ArrayList<>();
            int temp = k;
            for(int j=i;j<n;j++){
                if(nums[j]%p==0){
                    if(temp==0){
                        break;
                    }
                    temp--;
                }
                list.add(nums[j]);
                //每次都使用新的链表来存取数据
                List<Integer> temps = new ArrayList<>(list);
                set.add(temps);
            }
        }

        return set.size();
    }
}

运行结果:
在这里插入图片描述
由于第4题题目都来不及看了,因此在此就不进行复盘了,有兴趣的同学可以去看一下.

总结

本次周赛难度还是偏低的,但是我也因为第一题而有点慌了阵脚,思维还需要继续锻炼并强化自己的数据结构方面的知识能力!本次打榜全国排名3867,其实还是很有机会能够冲击前2000名的,主要是失误+知识点不牢固,希望能够再接再厉,一起加油~

D. Good Substrings time limit per test2 seconds memory limit per test512 megabytes You've got string s, consisting of small English letters. Some of the English letters are good, the rest are bad. A substring s[l...r] (1 ≤ l ≤ r ≤ |s|) of string s  =  s1s2...s|s| (where |s| is the length of string s) is string  slsl + 1...sr. The substring s[l...r] is good, if among the letters  sl, sl + 1, ..., sr there are at most k bad ones (look at the sample's explanation to understand it more clear). Your task is to find the number of distinct good substrings of the given string s. Two substrings s[x...y] and s[p...q] are considered distinct if their content is different, i.e. s[x...y] ≠ s[p...q]. Input The first line of the input is the non-empty string s, consisting of small English letters, the string's length is at most 1500 characters. The second line of the input is the string of characters "0" and "1", the length is exactly 26 characters. If the i-th character of this string equals "1", then the i-th English letter is good, otherwise it's bad. That is, the first character of this string corresponds to letter "a", the second one corresponds to letter "b" and so on. The third line of the input consists a single integer k (0 ≤ k ≤ |s|) — the maximum acceptable number of bad characters in a good substring. Output Print a single integer — the number of distinct good substrings of string s. Examples InputCopy ababab 01000000000000000000000000 1 OutputCopy 5 InputCopy acbacbacaa 00000000000000000000000000 2 OutputCopy 8 Note In the first example there are following good substrings: "a", "ab", "b", "ba", "bab". In the second example there are following good substrings: "a", "aa", "ac", "b", "ba", "c", "ca", "cb".
03-28
<think>嗯,用户问的是如何计算给定条件下包含至多k个坏字符的不同好子串的数量。这个问题看起来有点挑战性,不过我先得仔细理解问题。 首先,题目中的“好子串”应该是指满足某种条件的子串,而“坏字符”可能是预先定义好的某些特定字符。用户的问题是要统计所有不同的好子串的数量,其中每个子串最多包含k个坏字符。这里的“不同”应该指的是子串内容不同,而不仅仅是位置不同。 接下来,我需要回想一下类似的问题。比如LeetCode上的“至多包含K个不同字符的最长子串”这类滑动窗口的题目。可能这个题目也可以用滑动窗口来解决,但需要调整条件来处理坏字符的数量。 不过这里的不同之处在于,用户要统计的是所有可能的子串数量,而不是最长长度。所以可能需要遍历所有可能的子串,判断每个子串是否满足坏字符数量不超过k,同时还要去重。但是直接暴力枚举所有子串的话,时间复杂度会很高,特别是当字符串很长的时候,这显然不高效。 那如何高效计算呢?滑动窗口通常用来找满足条件的极值,比如最长或最短,但这里是要统计所有满足条件的子串数目。可能需要另一种方法,比如固定右端点,移动左端点,并记录符合条件的窗口,然后计算其中的子串数量,同时去重。 另外,需要考虑如何避免重复计数。比如,子串"ab"出现在不同位置是否算作不同的?根据用户的问题描述,“不同好子串”应该是指内容不同,所以即使出现在不同位置,只要内容相同就算同一个。这时候需要将子串存储在一个集合中,最后统计集合的大小。但这样对于长字符串来说,空间复杂度会很高,不可行。 所以可能需要找到一个方法,在不显式存储所有子串的情况下统计不同的子串数目。这时候想到使用字典树(Trie)或者滚动哈希(如Rabin-Karp算法)来记录出现过的子串,但实现起来可能比较复杂。 或者,问题是否允许位置不同的相同子串被算作多个?用户的问题描述可能不够明确。需要确认这一点。假设这里的“不同”是指内容不同,那么必须去重,否则的话,直接统计所有满足条件的子串数目即可。 假设是前者,即需要去重,那么这题就变得复杂了。例如,字符串"aaa"中,k=1,坏字符是'a',那么所有子串都是"a", "aa", "aaa",每个都包含坏字符的数量等于它们的长度,如果k>=3的话,那么这些子串都是有效的。但如果是坏字符不是a的话,情况又会不同。 不过用户的问题中,“坏字符”是预先定义的某些字符,每个子串中的坏字符数量不超过k。比如,假设坏字符是集合中的某些字符,子串中包含这些坏字符的数量不超过k。那如何统计所有不同的子串,其中坏字符的数量<=k? 这时候可能的思路是:遍历所有可能的子串,检查其中坏字符的数量是否<=k,同时将这些子串存入哈希集合去重,最后返回集合的大小。但这种方法的时间复杂度是O(n^2),对于n较大的情况无法处理,比如n=1e4的话,这样的算法会超时。 所以需要更高效的算法。可能需要结合滑动窗口哈希的方法,同时记录子串的出现情况。或者,是否有办法利用滑动窗口来统计所有满足条件的子串数目,同时去重? 另一种思路是,使用后缀自动机(Suffix Automaton)或者哈希的方式,来高效统计所有不同的子串。但结合坏字符的条件,可能需要先筛选出满足条件的子串,再统计不同的数目。这似乎比较复杂。 或者,可以先将字符串转换为坏字符的数量前缀数组,这样可以在O(1)时间内计算任意子串的坏字符数目。例如,前缀数组prefix,其中prefix[i]表示前i个字符中有多少个坏字符。这样,子串s[l..r]中的坏字符数目为prefix[r] - prefix[l]。这样,对于每个子串,可以快速判断是否满足条件。 接下来,问题转化为:找出所有不同的子串s[l..r],其中prefix[r] - prefix[l] <=k。同时统计这些子串的去重数量。 统计不同的子串数目,通常可以使用哈希或者后缀自动机。但如何在满足坏字符条件的情况下统计不同的子串数目? 可能的步骤: 1. 预处理坏字符的前缀数组。 2. 使用滑动窗口确定满足条件的子串的左右边界。 3. 对于每个可能的窗口,统计其中所有可能的子串,并去重。 但这样可能还是不够高效。或者,我们可以将问题拆解为:对于每个右端点r,找到最大的l,使得在[l, r]区间内的坏字符数目<=k,然后在所有可能的子串中以r结尾的,且满足条件的子串数目中,统计不同的子串数目。这似乎难以直接应用。 或者,考虑每个子串的起始位置结束位置,当滑动窗口维护一个区间[left, right],其中坏字符数目<=k。对于每个right,找到最小的left,使得[left, right]内的坏字符数目<=k。此时,以right结尾的子串中,起始位置可以从left到right,每个起始位置i到right的子串,是否都要考虑去重? 比如,对于每个right,我们需要找到所有可能的起始位置i,使得从i到right的子串满足坏字符数目<=k,然后将这些子串s[i..right]加入到哈希集合中。最后统计哈希集合的大小。这样时间复杂度是O(n^2)的,因为每个right可能对应O(n)个i,然后每个子串的哈希需要O(n)时间处理,总时间复杂度是O(n^3),这显然不可行。 所以这种方法对于长字符串来说效率太低,必须寻找更优的方法。 那有没有办法在滑动窗口的同时,记录不同的子串数目? 想到使用滚动哈希,比如对于每个位置,维护当前窗口内的所有子串的哈希值,并记录。但这可能复杂度依然很高。 另一种思路:滑动窗口确定当前窗口[left, right]是满足坏字符数目<=k的最长子串。那么,这个窗口内的所有子串都必须满足坏字符数目<=k吗?不是,比如,某个子串的起始位置在left左边,但可能坏字符数目更少。 所以滑动窗口可能无法直接用于统计所有子串。 或者,可以分步骤处理: 首先,找到所有可能的子串,其坏字符数目<=k,并且这些子串的内容是唯一的。然后统计这些的数量。 但如何高效找到所有这样的子串? 想到使用后缀树或后缀自动机,这些数据结构可以高效存储所有不同的子串。然后,遍历后缀自动机中的所有子串,检查是否满足坏字符数目<=k的条件。但如何在后缀自动机中快速判断每个子串的坏字符数目? 可能这需要预处理每个子串的出现位置,然后计算其中的坏字符数目,但这似乎不可行。 或者,可以结合滑动窗口哈希。滑动窗口维护一个区间,其中坏字符数目<=k。对于每个这样的窗口,我们可以枚举该窗口内的所有子串,并将它们加入哈希集合。例如,当窗口为[left, right],其中坏字符数目<=k,那么对于每个i属于[left, right],j属于[i, right],子串s[i..j]的坏字符数目是否一定<=k?不一定,因为可能i到j之间的坏字符数目可能超过k。例如,窗口[left, right]的坏字符数目刚好等于k,而i到right之间的坏字符数目可能更大。 所以这样的方法并不正确。 看来这个问题可能需要更深入的分析。或许我们可以将问题转化为,在满足坏字符数目<=k的条件下,统计所有不同的子串数目。这与常规的子串统计问题不同,因为这里有限制条件。 可能的解决方法是,将原问题拆分为两个部分: 1. 找到所有满足坏字符数目<=k的子串。 2. 在这些子串中统计不同的数目。 但如何高效完成这两部分? 对于第一部分,可以使用滑动窗口来找到所有可能的起始结束位置。例如,滑动窗口可以维护当前窗口[left, right]使得其中的坏字符数目<=k。对于每个right,确定最大的left,使得窗口内的坏字符数目<=k。此时,对于当前right,所有以right结尾的子串,其起始位置可以从这个left到right。然后,对于每个这样的子串,我们需要记录其字符串内容是否已经出现过。 但这样,对于每个right,需要处理O(right - left + 1)个子串,每个子串的长度可能不同,检查是否重复的时间复杂度很高。 因此,这种方法的时间复杂度可能高达O(n^2),当n较大时不可行。 有没有更好的方法?例如,使用Trie树来动态记录所有已经出现的子串,这样每次插入新的子串时可以快速判断是否重复。 例如,对于每个right,当滑动窗口的left确定后,我们可以从right往左遍历到left,将每个子串s[i..right]插入到Trie树中,并统计插入时是否新创建了节点,从而判断是否是新子串。这样,总的时间复杂度可能还是O(n^2),因为每个right可能要处理O(n)个字符,而Trie的插入是O(n)的时间复杂度。所以总的时间复杂度是O(n^3),这仍然不理想。 那有没有更高效的方法? 或者,可以考虑所有可能的满足条件的子串,并计算它们的哈希值,将这些哈希值存入一个集合,最后返回集合的大小。哈希的计算可以通过滚动哈希优化,比如使用前缀哈希数组,这样每个子串的哈希可以在O(1)时间内计算。例如,使用多项式滚动哈希: pre_hash[i]表示前i个字符的哈希值,那么子串s[i..j]的哈希可以计算为pre_hash[j+1] - pre_hash[i] * base^(j-i+1)。这样,每个子串的哈希可以快速得到,并且存入哈希集合中。 这样,对于每个可能的子串s[i..j],如果坏字符数目<=k,则计算其哈希并存入集合。最后集合的大小即为答案。 这种方法的时间复杂度是O(n^2),因为要枚举所有可能的ij,判断坏字符数目是否<=k。对于每个i,j,计算坏字符数目可以通过前缀数组,在O(1)时间完成。然后计算哈希是O(1),存入集合是O(1)。所以总时间复杂度是O(n^2),空间复杂度也是O(n^2)(哈希集合的大小)。 当n是1e4时,n²是1e8,这可能在时间上是可行的,但可能会超出内存限制。比如,每个哈希值存储为8字节,那么1e8个哈希值需要大约800MB,这在某些情况下可能不可行。所以需要更优的方法。 有没有办法将时间复杂度降低到O(n)或O(nk)? 假设我们使用滑动窗口来维护一个区间,其中坏字符数目<=k。对于每个右端点right,我们维护当前窗口的最左left,使得[left, right]中的坏字符数目<=k。此时,对于当前的right,所有可能的子串是s[left..right], s[left+1..right], ..., s[right..right]。这些子串的坏字符数目<=k吗? 不一定。例如,假设原窗口[left, right]的坏字符数目是m<=k,但其中某个子串s[i..right]的坏字符数目可能超过k。例如,原窗口的坏字符数目是m=3,k=3,而i=left+1,导致s[i..right]的坏字符数目可能还是3,或者在更小的情况下。因此,当原窗口的坏字符数目<=k时,其中的所有子串的坏字符数目可能也<=k。或者说,可能不是? 举个例子,假设原窗口的坏字符数目是3,k=3。那么,其中的任意子串的坏字符数目最多是3,但可能更少。所以原窗口内的所有子串都满足坏字符数目<=k的条件吗?不,不是的。例如,原窗口的坏字符数目是3,但其中的某个子串可能包含更多的坏字符?不可能,因为原窗口是整个父串的一部分,子串的坏字符数目不可能超过父串的。所以,如果父串的坏字符数目是m<=k,那么子串的坏字符数目一定<=m<=k。因此,原窗口内的所有子串的坏字符数目都<=k。 那这样的话,滑动窗口维护的区间[left, right]中的坏字符数目<=k。那么,对于每个right,对应的left是尽可能大的,即最大的left,使得窗口[left, right]的坏字符数目<=k。此时,所有以right为结尾的子串,其起始位置可以是left到right中的任意i,那么这些子串的坏字符数目一定<=k。因此,这些子串都是满足条件的。 这样,对于每个right,可以确定i的范围是[left, right],其中每个i对应的子串s[i..right]都是满足条件的。这样,我们需要统计这些子串中的不同数目。 此时,问题转化为:对于每个right,找到对应的left,然后统计所有s[i..right] (i从left到right) 中的不同子串数目,并将这些子串加入全局的集合中。 但如何高效统计这些子串的不同数目? 这时候想到使用后缀树或者哈希的方式,记录所有可能的子串。但如何高效处理? 或者,可以将问题转换为:所有满足坏字符数目<=k的子串的不同数目等于所有满足条件的子串的数目,减去重复的次数。但这样很难直接计算。 或者,可以采用一种增量方式。每次当right增加时,处理所有以right结尾的子串,这些子串的起始位置i在[left, right]范围内。对于每个这样的子串s[i..right],判断它是否已经出现过。如果没有,就计数加一。 但判断子串是否出现过的常规方法是使用哈希集合,存储所有子串的哈希值。但这样对于每个子串的时间复杂度是O(1),但存储哈希值需要O(n²)的空间,这在n较大的情况下不可行。 例如,当n=1e4时,子串数目是5e7左右,哈希集合的内存占用会非常大。 所以,必须寻找更高效的方法。 有没有办法利用滑动窗口的性质,结合字典树(Trie)来动态维护不同的子串? 例如,维护一个Trie,每次处理right时,从当前right开始,向左遍历到left,将每个子串s[i..right]插入到Trie中。插入过程中,如果路径已经存在,则不增加计数;否则,计数加一。这样,最终Trie的大小即为不同子串的数量。 这种方法的时间复杂度是O(n²),对于每个right,需要处理O(n)个字符插入到Trie中。每个插入操作的时间复杂度是O(length of substring)=O(n),所以总的时间复杂度是O(n³),这显然不可行。 因此,需要寻找更优的解决方案。 可能这个问题的难点在于如何高效统计满足条件的子串的不同数目,而不仅仅是数目。那么,是否有其他方式可以做到这一点? 想到利用后缀自动机(Suffix Automaton,SAM),它可以高效地表示一个字符串的所有不同子串,并且构建的时间复杂度是线性的。但如何结合坏字符的条件? SAM可以处理所有子串,但我们需要的是所有满足坏字符数目<=k的子串。因此,可能的思路是:构建整个字符串的SAM,然后遍历所有子串,计算它们的坏字符数目,统计满足条件的数目。但如何高效计算每个子串的坏字符数目? 这可能需要预处理每个可能的子串的坏字符数目,但如何高效查询? 或者,可以将字符串转换为坏字符的标记数组,比如将每个字符转换为0或1,表示是否是坏字符。然后,原问题转换为统计所有不同子串的数目,其中子串的(即坏字符的数量)<=k。同时,这些子串的原字符串内容必须不同。 这可能转换为一个二维问题:统计所有不同子串,且它们的区间<=k。这似乎比较复杂。 例如,假设将字符串转换为一个二进制数组bad,其中bad[i]=1表示s[i]是坏字符,否则为0。那么,问题转化为,找到所有不同的子串s[i..j],使得sum_{i<=p<=j} bad[p] <=k。 这类似于在数组中找到所有子数组的<=k,同时子数组对应的原字符串不同。但如何高效统计这样的数目? 这个问题可能还没有已知的高效解法,特别是在需要去重的情况下。 回到原问题,可能用户的问题中的“不同好子串”是否需要去重?比如,对于字符串"aba",子串"a"出现两次,是否算作两个不同的子串还是一个?根据用户的问题描述,可能需要去重,所以答案应该是2,因为两个不同的位置的"a"被视为相同的子串,所以只算一个。 所以,问题转化为统计所有不同的子串,其坏字符数目<=k。这可能需要一个高效的数据结构来记录所有不同的子串,同时满足条件。 此时,想到使用哈希的方法,结合滑动窗口。每次滑动窗口扩张到right时,处理所有可能的以right结尾的子串,并计算其哈希值,将满足条件的哈希值存入集合。这虽然空间复杂度高,但可能对于小规模的测试用例是可行的。 例如,对于每个right,确定left的位置,使得[left, right]内的坏字符数目<=k。然后,遍历i从left到right,生成子串s[i..right],计算其哈希值,并存入集合。最后,集合的大小即为答案。这种方法的时间复杂度是O(n²),空间复杂度是O(n²)。 对于n较大的情况,比如n=1e4,这可能需要处理1e8次哈希计算存储,这在实践中可能无法通过时间限制。 因此,可能需要寻找更优化的方法。 有没有办法将问题拆分成两个独立的部分:满足坏字符数目<=k,并且子串不同? 例如,使用滚动哈希,并且利用滑动窗口的性质,每次移动窗口时,维护当前窗口内的所有不同子串的哈希值。但如何高效地维护? 或者,考虑对于每个可能的起始位置i,维护一个最大的j,使得子串s[i..j]的坏字符数目<=k。然后,对于每个i,j的范围是i到某个最大值。此时,可以在每个i的范围内,统计不同子串的数目,并累加。但如何统计不同子串? 这可能又回到了O(n²)的时间复杂度问题。 或者,结合后缀自动机,预处理所有可能的子串,并记录每个子串的坏字符数目。这可能不可行,因为后缀自动机虽然可以高效处理不同子串的数目,但无法直接记录每个子串的属性,如坏字符数目。 另一个思路:对于每个可能的子串,其坏字符数目等于该子串中属于坏字符集的字符个数。因此,可以将问题转换为,统计所有不同子串,其中其中坏字符的数量<=k。 此时,可以将字符串转换为一个数组,其中每个元素是0或1(是否为坏字符),然后问题转化为统计不同的子串数目,其中子数组的<=k。但如何高效处理? 这可能与原始的统计不同子串数目的问题类似,但附加了一个条件:子数组的<=k。 目前,我没有想到已知的高效算法可以解决这个问题。因此,可能需要考虑这个问题是否有特定的约束条件,或者用户是否有其他思路。 例如,用户可能希望利用滑动窗口的方式,结合某种数据结构来维护当前窗口内的不同子串数目。 或者,可以尝试将问题分解为:首先,使用滑动窗口找到所有满足坏字符数目<=k的最长子串,然后对这些子串进行处理,统计其中的不同子串数目。但如何处理这些子串之间的重叠部分? 例如,假设有一个最长的窗口[left, right],其中坏字符数目<=k。那么,这个窗口内的所有子串都满足条件。但这些子串可能与之前窗口中的子串重复,因此需要去重。 但如何高效处理多个滑动窗口中的子串,并统计总数? 这似乎又回到之前的O(n²)方法。 综上,目前我认为,这个问题的最直接的解法是暴力枚举所有可能的子串,检查其坏字符数目是否<=k,并记录不同的子串数目。这种方法的时间复杂度是O(n²),空间复杂度也是O(n²)。当n不太大时(比如n<=1e3),这是可行的。但对于较大的n(如1e4),这种方法可能不可行。 因此,可能需要寻找更聪明的优化方法,或者利用某些特定的性质来减少计算量。 例如,当k=0时,问题转化为统计所有不包含任何坏字符的不同子串数目。这可能比较容易处理,比如找到所有连续的好字符区间,然后每个区间可以生成多个子串,统计这些子串的不同数目。 假设当k=0时,坏字符集合为B,那么所有子串必须不包含任何B中的字符。这时候,可以将原字符串分割为多个由好字符组成的段,每个段的长度为L,则该段贡献的不同子串数目为L*(L+1)/2。总共有sum(L*(L+1)/2)个子串。但此时需要将这些子串去重,因为不同的段可能有相同的子串。 哦,对,这是另一个问题:不同段中的子串可能有相同的,比如两个不同的段中都包含子串"ab"。因此,当k=0时,问题的答案并不是简单地将各段的子串数目相加,而是需要统计所有不同子串的总数。这显然需要更复杂的处理。 这说明,当k=0时,问题已经变得复杂,更不用说当k>0时的情况。 所以,回到原问题,可能目前没有已知的高效解法,只能给出O(n²)的暴力方法,并指出其适用范围。 或者,用户是否允许将相同内容但不同位置的子串视为不同的?例如,两个相同的子串出现在不同位置是否算作两个不同的子串? 这可能是一个关键点。如果用户的问题中,子串的位置不同但内容相同被视为同一个,那么必须去重。否则,如果视为不同的,则不需要去重,问题变得容易许多。 假设用户的问题中的“不同”指的是子串的内容不同,则必须去重。例如,字符串"abab"中,子串"ab"出现了两次,但只算一个。 在这种情况下,暴力法的复杂度很高,必须寻找更优的方法。 但根据用户的提问方式,可能他/她希望得到一个基于滑动窗口的高效解法,类似于LeetCode的某些问题。例如,可能该问题与统计不同子串数目有关,但结合了坏字符的条件。 这时,我可能需要参考相关的问题或论文。 例如,统计不同子串数目通常可以使用后缀自动机,其时间复杂度是O(n)。但如何结合坏字符的条件? 或许,可以构建后缀自动机,并在构建的过程中,记录每个状态对应的子串中的坏字符数目。例如,每个状态对应一组子串,这些子串的长度字符数目可以被跟踪。但这样的方法可能需要修改后缀自动机的结构,或者在后缀自动机构建完成后,进行额外的处理。 假设我们构建了整个字符串的后缀自动机,那么每个状态对应某些子串的集合。然后,我们需要遍历所有可能的子串,并计算其坏字符数目是否<=k,并统计这些数目。 但如何在遍历后缀自动机中的子串时,快速得到它们的坏字符数目? 这可能需要预处理每个子串的出现位置,并计算其中坏字符的数量。但如何高效地做到这一点? 或许,可以预处理前缀数组,然后在遍历后缀自动机中的每个子串时,根据其起始结束位置,计算坏字符数目。但后缀自动机中的子串并不直接记录其起始结束位置,而是通过长度链接来管理。 因此,这种方法可能不可行。 综上,我认为在目前的知识范围内,该问题可能没有比O(n²)更好的解法,特别是当必须去重时。因此,可能的回答是给出基于滑动窗口哈希集合的暴力方法,并指出其时间空间复杂度较高,适用于较小的n值。同时,可以探讨可能的优化方向,例如使用滚动哈希来减少存储开销,或者利用某些特定的字符串特性。 现在,我需要将上述思考过程整理成用户需要的回答,确保结构清晰,步骤明确,并且符合用户提供的引用内容。</think>### 解答思路 要计算包含至多$k$个坏字符的不同好子串数量,需结合滑动窗口哈希去重的思想。关键步骤包括窗口维护、坏字符计数子串去重[^3][^4]。 ### 步骤解析 1. **预处理坏字符标识** 将字符串转换为二进制数组`bad`,标记每个位置是否为坏字符: $$bad[i] = \begin{cases} 1 & s[i] \in \text{坏字符集合} \\ 0 & \text{否则} \end{cases}$$ 2. **滑动窗口维护有效区间** 使用双指针维护窗口$[left, right]$,保证窗口内坏字符数量$\leq k$。通过前缀数组快速计算坏字符数量: $$prefix[r] - prefix[l] \leq k$$ 3. **生成子串并去重** 对每个有效的窗口$[left, right]$,枚举以$right$结尾的子串$s[i..right]$($i \in [left, right]$),使用滚动哈希或字典树(Trie)去重记录子串。 ### 代码实现 ```python def count_good_substrings(s: str, bad_chars: set, k: int) -> int: n = len(s) bad = [0] * (n + 1) # 前缀数组 for i in range(n): bad[i+1] = bad[i] + (1 if s[i] in bad_chars else 0) unique_substrings = set() left = 0 for right in range(n): # 调整左边界,使窗口内坏字符数量 <=k while bad[right+1] - bad[left] > k: left += 1 # 生成所有以right结尾的子串,并去重 current_hash = 0 base = 128 # ASCII范围 mod = 10**18 + 3 # 大质数减少碰撞 for i in range(right, left-1, -1): current_hash = (current_hash * base + ord(s[i])) % mod unique_substrings.add(current_hash) return len(unique_substrings) ``` ### 复杂度分析 - **时间复杂度**:$O(n^2)$,每个右端点需反向遍历窗口生成子串 - **空间复杂度**:$O(n^2)$,哈希集合存储所有子串的哈希值 ### 优化方向 1. **滚动哈希优化**:使用双哈希减少碰撞概率[^3] 2. **后缀自动机**:构建SAM后筛选满足条件的子串,时间复杂度$O(n)$但实现复杂 3. **字典树压缩**:用Trie动态去重,减少内存占用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Astronaut_001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值