CF 316G3 Good Substrings——广义后缀自动机

本文介绍了一种使用后缀自动机解决特定编程竞赛问题的方法。通过对询问串和模式串一起构建后缀自动机,计算出每个字符串上的right集合大小,并通过遍历自动机节点来检查合法性。该方法有效解决了Codeforces平台上的一道题目。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目:http://codeforces.com/contest/316/problem/G3

对询问串和模式串一起建一个后缀自动机,做出在每个串上的 right 集合大小之后枚举自动机上的每个点看看是否合法即可(合法的话,贡献是 len[ cr ] - len[ fa ])。

注意做出拓扑序后用的是 q[ i ] 而不是 i !

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int N=550005,M=15,K=30;
int n,cnt=1,len[N],fa[N],go[N][K],ep[N];
int l[M],r[M],tx[N],q[N],ct[N][M]; ll ans;
char s[N]; bool vis[N];
int cz(int p,int w,int bh)
{
  int q=go[p][w], nq=++cnt; len[nq]=len[p]+1;
  fa[nq]=fa[q]; fa[q]=nq;
  memcpy(go[nq],go[q],sizeof go[q]);
  for(;go[p][w]==q;p=fa[p])go[p][w]=nq;
  return nq;
}
int ins(int p,int w,int bh)
{
  if(go[p][w])
    {
      int q=go[p][w];
      if(len[q]==len[p]+1)return q;
      return cz(p,w,bh);
    }
  int np=++cnt; len[np]=len[p]+1;
  for(;p&&!go[p][w];p=fa[p])go[p][w]=np;
  if(!p){fa[np]=1;return np;}
  int q=go[p][w]; if(len[q]==len[p]+1)fa[np]=q;
  else fa[np]=cz(p,w,-1);
  return np;
}
void Rsort()
{
  for(int i=1;i<=cnt;i++)tx[len[i]]++;
  for(int i=2;i<=cnt;i++)tx[i]+=tx[i-1];
  for(int i=1;i<=cnt;i++)q[tx[len[i]]--]=i;
}
bool chk(int cr)
{
  for(int i=1;i<=n;i++)
    if(ct[cr][i]<l[i]||ct[cr][i]>r[i])
      return false;
  return true;
}
int main()
{
  scanf("%s",s+1); int d=strlen(s+1);
  for(int i=1,p=1;i<=d;i++)
    p=ins(p,s[i]-'a'+1,0), ct[p][0]=1;
  scanf("%d",&n);
  for(int i=1;i<=n;i++)
    {
      scanf("%s%d%d",s+1,&l[i],&r[i]);
      d=strlen(s+1);
      for(int j=1,p=1;j<=d;j++)
    p=ins(p,s[j]-'a'+1,i), ct[p][i]=1;
    }
  Rsort();
  for(int i=cnt;i;i--)
    {
      int cr=q[i], f=fa[q[i]];////q[i] not i!!!
      for(int j=0;j<=n;j++)ct[f][j]+=ct[cr][j];
    }
  for(int i=2;i<=cnt;i++)
    if(ct[i][0]&&chk(i))
      ans+=len[i]-len[fa[i]];
  printf("%lld\n",ans);
  return 0;
}

 

转载于:https://www.cnblogs.com/Narh/p/10610598.html

D. Good Substrings time limit per test2 seconds memory limit per test512 megabytes You&#39;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&#39;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&#39;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&#39;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,坏字符是&#39;a&#39;,那么所有子串都是"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),因为要枚举所有可能的i和j,判断坏字符数目是否<=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&sup3;),这显然不可行。 因此,需要寻找更优的解决方案。 可能这个问题的难点在于如何高效统计满足条件的子串的不同数目,而不仅仅是数目。那么,是否有其他方式可以做到这一点? 想到利用后缀自动机(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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值