【KMP && 所有前缀,在原串出现次数(可重叠)和】HDU - 3336 Count the string

本文介绍了一种用于求解字符串中所有前缀在原串中出现次数(可重叠)的和的方法。通过构建字符串的next数组来高效地解决该问题,并提供了两种不同的实现方式:一种直接遍历求解,另一种采用记忆化搜索进行优化。

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

Step1 Problem:

给你字符串s, 求所有前缀,在原串出现次数(可重叠)和。

Step2 Ideas:

个人习惯从0开始,next[0] = 0;
next[i] : 代表下标从 0 到 i 这个子串,后缀 = 前缀 最长长度(不包括自身)。
枚举 i 从 0 到 len-1,对于每个 i 结尾的 所有后缀 = 前缀 的个数都求出来,然后求和。

Step3 Code:

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+100;
char s[N];
int nex[N];
void get_next()
{
    int len = strlen(s);
    nex[0] = 0;
    for(int i = 1; i < len; i++)
    {
        int j = nex[i-1];
        while(j && s[i] != s[j]) j = nex[j-1];
        if(s[i] == s[j]) nex[i] = j+1;
        else nex[i] = 0;
    }
}
int main()
{
    int T, n;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d %s", &n, s);
        get_next();
        int ans = n;
        for(int i = 0; i < n; i++)//枚举i
        {
            int t = i;
            while(nex[t]) {//求所有i结尾 后缀 = 前缀子串个数和
                ans++;
                t = nex[t-1];
            }
            ans %= 10007;
        }
        printf("%d\n", ans);
    }
    return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+100;
char s[N];
int nex[N];
void get_next()
{
    int len = strlen(s);
    nex[0] = 0;
    for(int i = 1; i < len; i++)
    {
        int j = nex[i-1];
        while(j && s[i] != s[j]) j = nex[j-1];
        if(s[i] == s[j]) nex[i] = j+1;
        else nex[i] = 0;
    }
}
int dp[N];//记忆化就不需要总是跑了
int dfs(int x) {
    if(nex[x] == 0) return 0;
    if(dp[x] != -1) return dp[x];
    return dp[x] = dfs(nex[x]-1) + 1;
}
int main()
{
    int T, n;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d %s", &n, s);
        get_next();
        for(int i = 0; i < n; i++) dp[i] = -1;
        int ans = n;
        for(int i = 0; i < n; i++)//枚举i
        {
            ans += dfs(i);
            ans %= 10007;
        }
        printf("%d\n", ans);
    }
    return 0;
}

<think>根据用户需求,我们需要计算字符串中所有长度为偶数的前缀在整个字符串中出现的次数。例如,字符串"ababa"中,长度为偶数的前缀有"", "ab", "abab"(注意:前缀长度包括0,但长度为0的前缀通常不计入,这里题目要求是偶数长度,所以包括长度为0,2,4,...的前缀)。注意,题目要求的是这些前缀在整个字符串中出现的次数(不仅仅是作为前缀出现,还可以在字符串中间出现)。 我们可以使用KMP算法中的next数组(或者称为部分匹配表)来帮助我们统计每个前缀出现的次数。但是,题目要求的是所有偶数长度前缀的出现次数。 思路: 1. 首先,我们需要计算字符串s的next数组(或者称为前缀函数)。next[i]表示子串s[0:i](长度为i的前缀)的最长相等前后缀的长度(不包括整个字符串本身)。 2. 但是,我们不仅仅需要最长前后缀,我们实际上需要统计每个前缀出现的次数。我们可以利用一个数组cnt来记录每个前缀出现的次数。注意,每个前缀至少出现一次(作为前缀出现)。 3. 我们可以利用next数组的性质进行递推:在构建next数组的过程中,我们可以知道,当我们在位置i(0-indexed)时,如果next[i]=j,那么说明前缀s[0:j](长度为j)在位置i以非前缀的形式又出现了一次(即作为后缀出现)。因此,我们可以通过倒序遍历next数组,将当前前缀出现的次数加到其最长前后缀对应的前缀上(因为出现当前前缀的地方,其最长前后缀也会出现)。 4. 具体步骤: a. 构建next数组:使用KMP算法构建next数组。 b. 初始化一个数组cnt,长度为n+1(n为字符串长度),cnt[i]表示长度为i的前缀出现的次数(初始化为1,因为每个前缀至少出现一次)。 c. 然后从后往前遍历next数组(从n到1),将cnt[i]的值加到cnt[next[i]]上。这是因为如果长度为i的前缀出现了cnt[i]次,那么它的最长前后缀(长度为next[i])也出现了cnt[i]次(因为每次出现长度为i的前缀时,其后缀部分必然出现一次长度为next[i]的前缀)。 d. 注意:这里我们统计的是所有前缀出现的次数,包括重叠出现的情况。 5. 最后,我们只需要将长度为偶数的前缀(即长度i为偶数,且i>=0,注意长度0我们通常不计,但题目要求是偶数长度,长度0也是偶数,但出现次数如何定义?题目中要求的是前缀,而长度为0的前缀(空串)在整个字符串中出现的次数应该是字符串长度+1(因为任意两个字符之间都可以认为出现了空串)?但是,题目要求的是“前缀”,所以这里我们只考虑作为前缀的字符串在整个字符串中出现的次数。注意,题目要求的是“在整个字符串中出现的次数”,所以空串应该出现很多次。然而,在KMP中我们通常不统计空串,所以我们需要明确: 题目要求:所有长度为偶数的前缀在整个字符串中出现的次数。 注意:这里的前缀是指从字符串开头开始的子串。但是,这些前缀(比如"ab")不仅可能出现在开头,还可能出现在字符串中间(比如在"abab"中,前缀"ab"在开头出现一次,在位置2也出现一次)。所以,我们统计的是每个这样的子串在整个字符串中出现的次数(包括重叠出现)。 但是,空串(长度为0)的情况?题目要求的是偶数长度,包括0吗?通常,题目中“长度”为0的子串(空串)在字符串中出现的次数是n+1(每个字符前后都可以有空串),但题目要求的是“前缀”,所以空串作为前缀只出现一次(在开头)?还是整个字符串中所有空串都算?题目没有明确,但是根据示例,我们通常只考虑非空串?然而题目要求的是“所有长度为偶数的前缀”,包括空串吗? 我们再看题目:求一个字符串中所有长度为偶数的前缀在整个字符串中出现的次数前缀的定义:从字符串开头开始的子串。所以,长度为0的前缀(空串)也是前缀。但是,在整个字符串中,空串出现的位置有很多。然而,题目要求的是“前缀”在整个字符串中出现的次数,这里“前缀”是否还是指必须从开头开始?不是,这里“在整个字符串中出现的次数”应该是指该子串作为任意子串(不一定是前缀)出现的次数。所以,空串应该出现n+1次(n为字符串长度)。 但是,我们通过KMP的cnt数组统计的是每个前缀(即每个以s[0]开头的子串)在整个字符串中出现的次数(包括重叠出现)。注意,我们统计的是这个子串(比如"ab")在字符串中任意位置出现的次数(包括重叠的,比如"aaa"中"aa"出现了两次:位置0位置1重叠)。 然而,我们通过上述方法(利用next数组递推)统计的cnt数组,实际上统计的是每个前缀(即每个s[0:i])作为子串出现的总次数(包括重叠)。这个方法在KMP算法中常见。 但是,空串(长度为0)并没有被包含在我们的next数组cnt数组中(因为我们通常从0开始,但长度为0的前缀我们一般不考虑在next数组里)。所以我们需要特别处理吗? 实际上,我们构建next数组时,通常next[0]是0,然后从i=1开始。而长度为0的前缀(空串)我们并没有一个对应的cnt[0]?所以我们需要定义cnt[0]吗? 让我们重新考虑: 我们构建next数组的过程: next[0] = 0; for (int i=1, j=0; i<n; ) { ... } 我们定义cnt数组下标从0到n,其中cnt[i]表示长度为i的前缀(即s[0:i])出现的次数。 注意:长度为0的前缀(空串)我们也要考虑。在KMP中,我们通常不显式处理空串,但我们可以认为空串的next值为-1(或者0)?实际上,我们可以将空串对应的索引设为0,然后长度为0的前缀出现次数是多少?按照上面的递推,我们最后需要统计所有偶数长度(包括0)的前缀的出现次数。 但是,在KMP的next数组构建中,我们通常没有空串的位置。我们可以这样: - 数组下标i(0-indexed)表示前缀的长度为i+1?不对,我们通常用next[i]表示s[0:i](长度为i)的前缀的最长相等前后缀的长度(这个长度是整数,不包括整个字符串本身,即小于i)。 所以,我们定义: next[0] = 0; // 长度为1的前缀的最长前后缀长度(显然为0,因为前后缀不能是自身) 然后,长度为0的前缀(空串)我们单独考虑:它的出现次数应该是整个字符串中空串出现的次数,即n+1(因为字符串有n个字符,所以有n+1个位置可以插入空串)。但是,题目要求的是前缀,而空串是前缀,所以我们需要统计。 然而,我们通过cnt数组递推只能得到非空前缀的出现次数。所以,我们需要单独加上空串的出现次数吗?但是题目要求的是所有偶数长度前缀,包括空串。所以,如果我们将空串的出现次数算作n+1,那么我们需要加上这个值。 但是,我们通过next数组递推得到的前缀出现次数是否包括空串?不包括。因此,我们可以在最后的结果中加上空串的出现次数(即n+1)?但是,我们也要注意:我们统计的是每个前缀(包括空串)的出现次数,而空串的出现次数是n+1,而其他偶数长度的前缀出现次数通过cnt数组得到。 然而,题目要求的是“所有长度为偶数的前缀”,所以我们需要: 结果 = 空串出现次数(n+1) + 所有长度为2,4,6,...的前缀出现次数。 但是,这里有一个问题:空串出现次数n+1是否合理?题目要求的是“前缀”在整个字符串中出现的次数,而空串作为前缀只出现在开头?还是整个字符串中任意位置出现的空串都算?题目没有明确,但通常,子串出现包括空串,且空串在任意两个字符之间都出现。 然而,题目要求的是“前缀”,而空串是前缀,但它在整个字符串中出现的次数不限于作为前缀的那一次,而是任意位置。所以,整个字符串中空串出现了n+1次(包括开头、结尾每两个字符之间)。 因此,我们需要将空串的出现次数(n+1)计入答案。 但是,我们也要注意:其他前缀(非空)的出现次数是否包括了作为子串出现的所有情况(包括重叠)?是的,通过cnt数组递推,我们统计了每个非空前缀出现的总次数(包括重叠)。 所以,步骤: 1. 计算字符串长度n。 2. 如果n==0,则直接返回1(空串出现1次?但空串在空字符串中出现1次?实际上,空串在空字符串中出现1次(在开头),但按照定义,空串在空字符串中出现次数为1。而题目要求整个字符串中出现的次数,对于空字符串,长度为0,那么空串出现1次。但题目没有说明,我们按照通常定义:空串在任何字符串中出现次数为字符串长度+1。所以对于长度为n的字符串,空串出现次数为n+1)。 3. 构建next数组,同时初始化cnt数组(长度为n+1,每个元素初始化为1)。 4. 使用KMP算法构建next数组: next[0] = 0; cnt[0] = 1; // 注意:这里cnt[0]表示长度为0的前缀(空串)?不对,我们通常用cnt[i]表示长度为i的前缀出现的次数。但是,在KMP中,我们通常从0开始,但0长度我们并没有在next数组中处理。所以,我们构建next数组时,下标i从0到n-1,对应的是长度为1到n的前缀。因此,我们需要单独处理长度为0的前缀(空串)吗? 重新设计: 我们定义: 字符串s,长度为n。 我们构建next数组,next[i]表示子串s[0:i](即长度为i+1的前缀)的最长相等前后缀的长度?或者另一种常见定义:next[i]表示子串s[0:i-1]的最长前后缀?实际上,KMP的next数组有多种构建方式。 为了避免混淆,我们采用如下定义(也是常见的一种): 令next[0] = -1; // 或者0,但这里我们采用另一种方式:next[i]表示对于前缀s[0:i](长度为i+1)的最长相等前后缀的长度(这个长度小于i+1)。但是,为了方便,我们通常使用这样的方式: 另一种更常见的定义(也是我们之前引用中提到的匹配表): 我们定义数组next,其中next[i]表示子串s[0:i-1](即长度为i的前缀)的最长相等前后缀的长度(即前缀函数)。这样,我们通常将next数组的长度设为n,其中next[0]没有意义或者为0(因为长度为1的前缀没有真前缀真后缀)。 但是,为了方便递推cnt数组,我们采用如下方式: 定义数组next,长度为n(对应字符串的每个位置0~n-1),其中next[i]表示子串s[0:i](即长度为i+1)的最长相等前后缀的长度(这个长度是0~i之间的整数)。注意,这个最长前后缀不能是整个子串本身,所以长度小于i+1。 然后,我们定义cnt数组,长度为n+1,其中cnt[i]表示长度为i的前缀(即s[0:i-1],当i>=1时)出现的次数。但是,我们也要考虑长度为0的前缀(空串),所以我们让cnt[0]表示空串出现的次数?但是,在KMP中,我们通常不处理空串,所以我们可以单独处理空串。 为了统一,我们这样: 我们只统计非空的前缀(即长度>=1)的出现次数,而空串单独处理。 因此,算法步骤: 1. 初始化: n = s.length(); vector<int> next(n, 0); // next数组,长度n,初始化为0 vector<int> cnt(n+1, 0); // cnt[i]表示长度为i的前缀出现的次数,i从1到n,但数组下标0我们不用?或者我们让cnt[i]表示长度为i的前缀(i>=1)出现的次数。但是,我们也要处理长度为0的,所以我们可以: // 单独处理空串:空串出现次数 = n+1 long long ans = n+1; // 先加上空串的出现次数 2. 构建next数组,同时初始化cnt数组(对于非空前缀,初始出现次数为1): // 注意:每个非空前缀至少出现一次(作为前缀) for (int i=1; i<=n; i++) { cnt[i] = 1; } // 构建next数组,同时我们还需要一个辅助数组,用于后面的递推?实际上,构建next数组递推cnt数组是分开的。 // 先构建next数组(标准KMP构建) next[0] = 0; // 长度为1的前缀,其最长相等前后缀长度为0 for (int i=1, j=0; i<n; ) { if (s[i] == s[j]) { next[i] = j+1; // 注意:这里next[i]表示前缀s[0:i]的最长相等前后缀长度 j++; i++; } else { if (j == 0) { next[i] = 0; i++; } else { j = next[j-1]; // 回退,注意:这里j-1可能为0,所以需要判断 } } } // 但是,上面的构建方式可能不是最标准的,因为不同资料有不同写法。我们也可以这样: // 另一种常见的写法: next[0] = 0; int j = 0; for (int i=1; i<n; i++) { while (j>0 && s[i]!=s[j]) { j = next[j-1]; } if (s[i]==s[j]) { j++; } next[i] = j; } 3. 然后,我们倒序更新cnt数组(从大到小): // 注意:next[i]表示前缀s[0:i](长度为i+1)的最长相等前后缀的长度(即next[i]的值) // 但是,我们cnt数组的下标是前缀的长度,所以对于位置i,前缀长度为i+1,其最长前后缀长度为next[i](这个值表示的长度就是前后缀的长度,比如next[i]=k,那么前后缀的长度为k,对应前缀s[0:k-1](长度为k))。 // 因此,当我们有长度为i+1的前缀出现时,其最长前后缀(长度为next[i])也会出现。 // 所以,我们从后往前遍历(从n-1到0),将cnt[i+1]的值加到cnt[next[i]]上。 // 但是,我们的cnt数组下标是长度,所以: for (int i = n-1; i > 0; i--) { // 注意:i从n-1到1(0不需要,因为next[0]=0,而长度为1的前缀的最长前后缀长度为0,0对应空串?但我们的cnt数组没有0这个长度?不对,我们的cnt数组下标0表示长度为0?不对,我们之前只定义了cnt[0]没有使用?) // 重新考虑:我们定义cnt数组,下标0表示长度为0(空串)?但是我们没有统计空串的传递。所以,我们让cnt数组的下标i表示长度为i的前缀。 // 而next[i]的值表示的是长度为i+1的前缀的最长相等前后缀的长度(这个长度值就是我们要的另一个前缀的长度)。因此,对于位置i(0-indexed),我们处理的是长度为i+1的前缀,其出现次数为cnt[i+1](初始为1),然后我们要将其加到长度为next[i]的前缀的出现次数上。 // 所以,循环: for (int i = n-1; i >= 1; i--) { // 注意:i从n-1到1,因为next[i]中i>=1,而i=0时next[0]=0,我们不需要处理长度为1的前缀?不对,我们需要处理所有非空的前缀。 // 但是,注意:当i=0时,对应长度为1的前缀,但next[0]=0,所以我们要将cnt[1]加到cnt[0]上(即空串)?所以我们需要从0开始?不对,我们循环从n-1到0。 // 修改:从后往前遍历所有非空的前缀(长度从大到小,即下标从n-1到0) for (int i = n-1; i >= 0; i--) { // 如果next[i]>0,说明有非空的最长前后缀 if (next[i] > 0) { cnt[next[i]] += cnt[i+1]; // 注意:这里i+1是当前前缀的长度,因为位置i对应前缀长度i+1 } } // 但是,上面的循环中,我们使用的是next[i](一个整数)作为下标,而next[i]表示长度,所以可以直接作为cnt数组的下标(即cnt数组的下标就是长度)。 // 但是,注意:我们的cnt数组下标范围是0~n,所以next[i]必须在0~n之间,而next[i]最大为i(因为长度不能超过i+1,而next[i]是前后缀长度,所以小于等于i+1,但实际最大为i,因为不能等于整个串长度)。 // 但是,上面的循环中,我们使用的是i(从0到n-1),而cnt[i+1]表示长度为i+1的前缀出现的次数。然后,我们将其加到cnt[next[i]]上(即它的最长前后缀对应的前缀的出现次数上)。 // 注意:这里为什么要倒序?因为如果先处理长的前缀,那么它的出现次数可以传递给它对应的前后缀(即一个短的前缀),然后再处理短的前缀时,它可能又传递到更短的前缀。这样保证每个前缀的出现次数都被传递到所有包含它的前后缀。 4. 然后,我们遍历所有长度为偶数的非空前缀(长度为2,4,...,不超过n),将它们的cnt值相加,再加上空串的n+1,得到最终答案。 for (int len=2; len<=n; len+=2) { ans += cnt[len]; } 但是,这里有一个问题:在步骤3中,我们更新cnt数组时,使用的是位置i(0-indexed)对应的前缀长度i+1,而cnt数组的下标就是长度。所以,cnt[len]就是长度为len的前缀出现的总次数。 然而,我们上面的更新过程是否正确?我们来看一个例子:字符串"aaa"(n=3) 空串出现次数:3+1=4(先加上) 非空前缀: 长度1: "a" -> 出现次数cnt[1]初始为1,更新后? 长度2: "aa" -> 初始为1 长度3: "aaa" -> 初始为1 构建next数组: i=0: next[0]=0 (前缀"a") i=1: s[1]=='a',j=0,比较s[1]s[0],相等,所以next[1]=1 (前缀"aa"的最长相等前后缀长度为1,即"a") i=2: s[2]=='a',j=1(因为上一个i=1时j=1),比较s[2]s[1]相等,所以next[2]=2? 不对,应该是:在计算next[2]时,j=1,s[2]==s[1]? 这里s[1]是第二个字符,而j=1表示我们已经匹配了1个字符(即前缀"aa"),然后现在匹配下一个字符,所以j=1,然后s[2]==s[1](都是'a'),所以j++变成2,然后next[2]=j=2。但实际上,前缀"aaa"的最长相等前后缀应该是"aa"(长度为2)?不对,应该是"aa"(因为前后缀不能是自身,所以最长应该是2?但是,前后缀可以重叠吗?注意,前后缀的定义:前缀是连续子串的开头一部分,后缀是连续子串的结尾一部分。对于"aaa",其相等前后缀有"a"(长度为1)"aa"(长度为2),所以最长是2。 所以next数组为:next[0]=0, next[1]=1, next[2]=2. 然后,我们倒序更新cnt数组: i=2: 对应长度3,next[2]=2>0,所以cnt[2] += cnt[3] -> cnt[2] = 1 + 1 = 2 i=1: 对应长度2,next[1]=1>0,所以cnt[1] += cnt[2] -> cnt[1] = 1 + 2 = 3 i=0: 对应长度1,next[0]=0,不操作。 然后,我们统计偶数长度(2)的前缀出现次数:cnt[2]=2,再加上空串的4,总数为6。 但是,我们验证一下: 空串:出现4次(位置0,1,2,3) 长度为2的前缀"aa":在整个字符串中出现了几次? 位置0: "aa"(s[0:1]) -> 1次 位置1: "aa"(s[1:2]) -> 1次 位置2: 不能形成"aa"(因为字符串结束) 所以一共2次。 所以总数=4+2=6,正确。 另外,长度为1的前缀"a":出现了3次(位置0,1,2)?但是题目要求偶数长度,所以不统计。 但是,注意:我们统计长度为2的前缀"aa"时,我们只统计了作为长度为2的前缀(即整个字符串的前两个字符)出现的次数?不对,我们通过cnt[2]得到的是整个字符串中"aa"作为子串出现的次数(包括重叠),所以确实是2次。 所以,这个例子正确。 但是,另一个例子:字符串"ababa"(n=5) 空串:5+1=6 构建next数组: s = "ababa" next[0]=0 i=1: j=0, s[1]='b' != s[0]='a' -> j=next[j-1] 但是j=0,所以next[1]=0, j保持0,i++到2 i=2: j=0, s[2]='a' == s[0]='a' -> next[2]=1, j=1, i=3 i=3: j=1, s[3]='b' == s[1]='b' -> next[3]=2, j=2, i=4 i=4: j=2, s[4]='a' == s[2]='a' -> next[4]=3, j=3, 结束 next数组: [0,0,1,2,3] 更新cnt数组(初始cnt[1..5]=1): 倒序: i=4: 长度5,next[4]=3>0 -> cnt[3] += cnt[5] -> cnt[3]=1+1=2 i=3: 长度4,next[3]=2>0 -> cnt[2] += cnt[4] -> cnt[2]=1+1=2 i=2: 长度3,next[2]=1>0 -> cnt[1] += cnt[3] -> cnt[1]=1+2=3 i=1: 长度2,next[1]=0 -> 不操作 i=0: 长度1,next[0]=0 -> 不操作 然后,偶数长度:长度24 cnt[2]=2, cnt[4]=1(注意,在更新过程中,cnt[4]在i=3时被用于更新cnt[2],但cnt[4]本身还是初始值1?不对,在更新过程中,我们只将当前长度对应的cnt值加到其next值对应的长度上,并没有改变当前长度对应的cnt值?不对,我们更新的是next[i]对应的cnt,而当前长度i+1的cnt值在后续可能被其他更长的前缀传递吗?不会,因为我们是倒序(从长到短)更新,所以每个长度只被传递一次。 但是,注意:我们的更新顺序是从长到短,所以当我们更新长度5时,我们更新了长度3(cnt[3]变为2);然后更新长度4时,我们更新了长度2(cnt[2]变为1+1=2);然后更新长度3时,我们更新了长度1(cnt[1]变为1+2=3)。注意,长度4的cnt值在更新过程中没有被其他长度传递,所以保持1。 所以,偶数长度:长度2->2次,长度4->1次,再加上空串6次,总共6+2+1=9。 验证: 空串:6次(0,1,2,3,4,5) 长度为2的前缀"ab":在整个字符串中出现了几次? 位置0: "ab" -> 1次(作为前缀) 位置2: "ab"(s[2:3]是"a"s[3:4]是"b"?不对,s[2:3]是"a",s[3:4]是"b",所以"ab"出现在位置2? 但是s[2:4]="ab" -> 1次 位置0: "ab"(0,1)位置2: "ab"(2,3) -> 共2次。 长度为4的前缀"abab":出现了几次? 位置0: "abab" -> 1次(作为前缀) 其他地方:字符串"ababa",在位置0之后,只有位置1开始的"baba"位置2开始的"aba"等,没有"abab"。 所以只有1次。 所以总数=6+2+1=9,正确。 但是,注意:长度为2的前缀"ab"在位置0位置2出现,共2次;而长度为4的前缀"abab"只在位置0出现1次。 因此,代码实现: 步骤: 1. 读入字符串s,n = s.length() 2. 如果n==0,则直接返回1(空串出现1次)?不对,空串在空字符串中出现1次,但题目要求的是整个字符串中出现的次数,空串出现次数为n+1,所以当n=0时,返回1(即0+1=1)是合理的。 3. 初始化next数组(长度为n,初始为0),cnt数组(长度为n+1,初始为0) 4. 将cnt数组从1到n都初始化为1(每个非空前缀至少出现一次) 5. 构建next数组(使用KMP算法) 6. 倒序更新cnt数组:for (int i=n-1; i>=0; i--) if (next[i] > 0) { cnt[next[i]] += cnt[i+1]; // 注意:这里i+1表示当前前缀的长度(因为位置i对应前缀长度i+1),而next[i]是一个长度值(即前后缀的长度) } 7. 统计答案:ans = n+1(空串) + 所有偶数长度len(2,4,...,不超过n)的cnt[len]之。 注意:这里cnt数组的下标就是前缀长度,所以直接遍历偶数长度即可。 但是,注意:在步骤6中,我们更新cnt数组时,我们使用的是cnt[i+1](即长度为i+1的前缀的出现次数)去更新cnt[next[i]](即长度为next[i]的前缀的出现次数)。而next[i]的值在0到i之间(因为最长前后缀长度不能超过i+1,但实际最大为i,因为不能等于整个串)。 另外,为什么倒序?因为一个前缀可能在多个更长前缀中作为前后缀出现,所以我们要从长的前缀开始传递,这样保证每个前缀的出现次数都传递到了所有更短的前缀上。 最后,注意:长度为0的前缀(空串)我们单独处理(n+1),而cnt数组只处理长度1~n的前缀。 代码实现: 注意:在构建next数组时,我们使用常见的KMP前缀函数构建方法。 但是,我们上面的构建方法(如下): next[0] = 0; int j = 0; for (int i=1; i<n; i++) { while (j>0 && s[i]!=s[j]) { j = next[j-1]; } if (s[i]==s[j]) { j++; } else { j = 0; } next[i] = j; } 这个构建方法中,当s[i]!=s[j]且j==0时,我们设置j=0(然后next[i]=0)。这我们之前的手动计算一致。 因此,我们使用这种方法构建next数组。 编写C++代码: 注意:字符串s,下标0开始。 但是,注意:在倒序更新cnt数组时,我们使用next[i]作为下标,而next[i]可能为0,此时我们不更新(因为next[i]>0才更新)。 最后,我们输出答案。 测试用例: 空字符串:返回1 字符串"aa":n=2 next[0]=0 i=1: j=0, s[1]=='a'==s[0]=='a' -> j++ -> j=1, next[1]=1 更新cnt:i=1: 长度2,next[1]=1>0 -> cnt[1] += cnt[2] -> cnt[1]=1+1=2 i=0: 不更新 答案 = 2+1(空串)+ cnt[2](偶数长度2)? 不对,我们只统计偶数长度,长度2:cnt[2]=1(初始为1,在更新过程中,长度2的cnt值没有被更新,因为更新的是cnt[1]) -> 所以偶数长度:长度2的cnt[2]=1,长度0(空串)3(即2+1)?不对,空串出现次数是n+1=3,然后再加上偶数非空:长度2->1,所以总答案为3+1=4。 验证: 空串:3次(0,1,2) 长度为2的前缀"aa":出现1次(位置0) 所以总次数=3+1=4,正确。 但是,注意:我们统计偶数长度前缀时,长度0(空串)已经单独加了(n+1),然后我们再加上所有偶数非空前缀(长度>=2)的出现次数。所以,对于"aa",偶数长度有长度0(空串)长度2。空串我们已经加了3,长度2的cnt[2]=1,所以总答案=3+1=4。 所以,代码: 注意:cnt数组的下标0我们并没有使用(因为长度为0的前缀我们单独处理了)。在更新过程中,当next[i]等于0时,我们不更新,因为0对应空串?但是,我们并没有将长度1的前缀的出现次数传递给空串?实际上,在更新过程中,如果next[i]==0,我们就不传递。所以,空串的出现次数只包括n+1,不包括其他传递? 这是合理的,因为空串不是任何非空前缀的真前后缀(因为真前后缀要求非空?)。在KMP中,前后缀通常是非空的,所以next[i]>=1表示非空前后缀。 因此,我们按上述步骤编写代码。 但是,注意:在构建next数组时,我们使用的是0-indexed,而next[i]的值表示的是前缀s[0:i]的最长相等前后缀的长度(即这个长度值可以直接作为cnt数组的下标,表示一个前缀的长度)。 代码实现: 注意:cnt数组的大小为n+1(下标0~n),其中cnt[i]表示长度为i的前缀出现的次数(i>=1),而cnt[0]我们不用(或者我们用来表示空串?不对,空串我们单独处理,所以不用cnt[0])。但是,在更新时,我们可能遇到next[i]等于0,此时我们不更新,因为0对应空串,而空串我们已经单独处理了(n+1),并且我们不希望再通过传递来增加空串的出现次数(因为空串的出现次数已经固定为n+1,而且传递可能会重复计算?)。所以,我们不更新next[i]为0的情况。 但是,在更新过程中,我们可能将出现次数传递到长度大于0的前缀,所以只更新next[i]>0的情况。 最后,我们遍历长度2,4,...,n(偶数)的cnt值,累加。 代码: 注意:如果n为0,则直接返回1(空串出现1次)。 我们假设字符串s。 代码: int n = s.size(); if (n == 0) { return 1; // 空串出现1次 } vector<int> next(n, 0); vector<long long> cnt(n+1, 0); // 使用long long防止溢出 // 初始化cnt:每个非空前缀至少出现1次 for (int i=1; i<=n; i++) { cnt[i] = 1; } // 构建next数组 next[0] = 0; // 长度为1的前缀 int j = 0; for (int i=1; i<n; i++) { while (j>0 && s[i]!=s[j]) { j = next[j-1]; } if (s[i] == s[j]) { j++; } else { j = 0; // 这里其实可以省略,因为j已经是0了,但为了清晰 } next[i] = j; } // 倒序更新cnt数组:从最后一个字符(下标n-1)开始 for (int i = n-1; i >= 0; i--) { if (next[i] > 0) { cnt[next[i]] += cnt[i+1]; // 注意:i+1是当前前缀的长度 } } // 计算答案:空串出现次数n+1 + 所有偶数长度(从2到n,步长2)的前缀出现次数 long long ans = n+1; // 空串 for (int len=2; len<=n; len+=2) { ans += cnt[len]; } return ans; 测试:字符串"aaa"(n=3)-> 空串4,偶数长度:2->cnt[2]=? 在更新后cnt[2]=2(因为长度3传递给了长度2,而长度2初始为1,然后长度3的更新:i=2时,next[2]=1(因为next[2]等于1?不对,我们上面计算"aaa"的next数组:next[0]=0, next[1]=1, next[2]=2? 我们上面的代码构建的next数组: s="aaa" i=1: j=0, s[1]=='a'==s[0]=='a' -> j=1, next[1]=1 i=2: j=1, s[2]=='a'==s[1]=='a' -> j=2, next[2]=2 所以,更新: i=2: next[2]=2>0 -> cnt[2] += cnt[3] -> cnt[2] = 1+1=2 i=1: next[1]=1>0 -> cnt[1] += cnt[2] -> cnt[1] = 1+2=3 i=0: next[0]=0 -> 不更新 然后,偶数长度:长度2->cnt[2]=2,长度3是奇数跳过。 所以答案=4+2=6,正确。 但是,注意:在更新cnt[1]时,我们用的是cnt[2](此时cnt[2]已经被更新为2),所以cnt[1]变成3。 但是,长度为2的前缀出现次数是2次(即"aa"出现2次),正确。 因此,我们输出代码。 注意:题目要求的是整个字符串中出现的次数,包括重叠出现。 最后,我们输出答案。 但是,注意:cnt数组的类型是long long,因为次数可能很大。 完整代码: 我们将代码封装为一个函数。 函数原型:long long countEvenPrefix(const string& s) 测试用例: s="aba" -> n=3 next数组:next[0]=0 i=1: j=0, s[1]='b'!=s[0]='a' -> j=0, next[1]=0 i=2: j=0, s[2]='a'==s[0]='a' -> j=1, next[2]=1 更新cnt: i=2: next[2]=1>0 -> cnt[1] += cnt[3] -> cnt[1]=1+1=2 i=1: next[1]=0 -> 不更新 i=0: next[0]=0 -> 不更新 偶数长度:长度2:cnt[2]=1(初始值,未被更新) -> 所以答案 = (3+1) + 1 = 5? 验证: 空串:4次(0,1,2,3) 长度为2的前缀"ab":出现1次(位置0) 所以总次数=4+1=5,正确。 但是,注意:长度为2的前缀"ab"在字符串中只出现一次(在位置0),而位置1的"ba"不是"ab"。 所以,代码正确。 但是,注意:在更新过程中,长度为3的前缀"aba"的出现次数传递给了长度为1的前缀"a"(因为next[2]=1),所以"a"的出现次数变为2(初始1+1)。但是,题目要求的是偶数长度前缀,所以不统计长度为1的前缀。 因此,我们只关心偶数长度的前缀。 现在,我们编写完整代码。 注意:题目要求使用KMP算法,我们使用了KMP的next数组,并且利用next数组进行传递,统计出现次数。 返回答案。 代码:</think>以下是使用KMP算法统计字符串中所有偶数长度前缀出现次数的C++实现。算法核心是通过KMP的next数组递推统计每个前缀的出现次数,最后累加偶数长度前缀的出现次数(包括空串的$n+1$次)。 ```cpp #include <iostream> #include <vector> #include <string> using namespace std; long long countEvenPrefix(const string& s) { int n = s.size(); // 空串情况:空串出现次数为1(n=0时n+1=1) if (n == 0) return 1; vector<int> next(n, 0); // KMP的next数组 vector<long long> cnt(n + 1, 0); // cnt[i]:长度为i的前缀出现次数 // 初始化:每个非空前缀至少出现1次 for (int i = 1; i <= n; i++) { cnt[i] = 1; } // 构建next数组(标准KMP算法) next[0] = 0; int j = 0; for (int i = 1; i < n; i++) { while (j > 0 && s[i] != s[j]) { j = next[j - 1]; } if (s[i] == s[j]) { j++; } else { j = 0; } next[i] = j; } // 倒序更新cnt数组:从长前缀向短前缀传递出现次数 for (int i = n - 1; i >= 0; i--) { if (next[i] > 0) { cnt[next[i]] += cnt[i + 1]; // next[i]是长度,i+1是当前前缀长度 } } // 计算总:空串出现n+1次 + 所有偶数长度非空前缀 long long ans = n + 1; // 空串出现次数 for (int len = 2; len <= n; len += 2) { ans += cnt[len]; } return ans; } int main() { string s; cout << "输入字符串: "; cin >> s; cout << "偶数长度前缀总出现次数: " << countEvenPrefix(s) << endl; return 0; } ``` ### 算法说明 1. **KMP的next数组**:`next[i]`表示前缀`s[0..i]`的最长相等前后缀长度(不包含自身)。例如: - `s="aaa"` → `next=[0,1,2]` - `s="ababa"` → `next=[0,0,1,2,3]` 2. **出现次数递推**: - 初始化`cnt[i]=1`(每个前缀至少作为前缀出现1次) - 倒序遍历`next`数组:若`next[i] > 0`,则将当前前缀`s[0..i]`的出现次数累加到其最长前后缀`s[0..next[i]-1]`上 - 例:`s="aaa"`中,`cnt[3]=1` → 传递到`cnt[2]`使`cnt[2]=2`,再传递到`cnt[1]=3` 3. **偶数长度前缀统计**: - 空串出现次数固定为$n+1$(在字符串的$n+1$个位置出现) - 非空偶数长度前缀:遍历长度$2,4,\ldots$,累加`cnt[len]` ### 示例验证 - `s="aa"`: - 空串:$2+1=3$次 - 长度2前缀`"aa"`:出现1次 -$=3+1=4$ - `s="ababa"`: - 空串:$5+1=6$次 - 长度2前缀`"ab"`:出现2次 - 长度4前缀`"abab"`:出现1次 -$=6+2+1=9$ ### 时间复杂度 - 构建next数组:$O(n)$ - 递推cnt数组:$O(n)$ -:$O(n)$ - **总复杂度**:$O(n)$,优于暴力算法的$O(n^2)$ ### 相关问题 1. 如何证明KMP算法中cnt数组递推的正确性? 2. 如果要求统计奇数长度前缀的出现次数,应如何修改代码? 3. KMP算法中next数组的构建过程能否优化? 4. 该算法在哪些字符串匹配场景中具有显著优势? 5. 如何处理包含Unicode字符的字符串前缀统计问题? [^1]: KMP算法通过next数组避免回溯,高效统计子串出现次数 [^2]: 递推优化利用部分匹配信息传递出现次数 [^3]: 前缀定义包含空串完整子串,需统一处理边界
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值