P7114 [NOIP2020] 字符串匹配 题解

390 篇文章 1 订阅
83 篇文章 1 订阅

[NOIP2020] 字符串匹配

题目描述

小 C 学习完了字符串匹配的相关内容,现在他正在做一道习题。

对于一个字符串 S S S,题目要求他找到 S S S 的所有具有下列形式的拆分方案数:

S = A B C S = ABC S=ABC S = A B A B C S = ABABC S=ABABC S = A B A B … A B C S = ABAB \ldots ABC S=ABABABC,其中 A A A B B B C C C 均是非空字符串,且 A A A 中出现奇数次的字符数量不超过 C C C 中出现奇数次的字符数量。

更具体地,我们可以定义 A B AB AB 表示两个字符串 A A A B B B 相连接,例如 A = aab A = \texttt{aab} A=aab B = ab B = \texttt{ab} B=ab,则 A B = aabab AB = \texttt{aabab} AB=aabab

并递归地定义 A 1 = A A^1=A A1=A A n = A n − 1 A A^n = A^{n - 1} A An=An1A n ≥ 2 n \ge 2 n2 且为正整数)。例如 A = abb A = \texttt{abb} A=abb,则 A 3 = abbabbabb A^3=\texttt{abbabbabb} A3=abbabbabb

则小 C 的习题是求 S = ( A B ) i C S = {(AB)}^iC S=(AB)iC 的方案数,其中 F ( A ) ≤ F ( C ) F(A) \le F(C) F(A)F(C) F ( S ) F(S) F(S) 表示字符串 S S S 中出现奇数次的字符的数量。两种方案不同当且仅当拆分出的 A A A B B B C C C 中有至少一个字符串不同。

小 C 并不会做这道题,只好向你求助,请你帮帮他。

输入格式

本题有多组数据,输入文件第一行一个正整数 T T T 表示数据组数。

每组数据仅一行一个字符串 S S S,意义见题目描述。 S S S 仅由英文小写字母构成。

输出格式

对于每组数据输出一行一个整数表示答案。

样例 #1

样例输入 #1

3
nnrnnr
zzzaab
mmlmmlo

样例输出 #1

8
9
16

样例 #2

样例输入 #2

5
kkkkkkkkkkkkkkkkkkkk
lllllllllllllrrlllrr
cccccccccccccxcxxxcc
ccccccccccccccaababa
ggggggggggggggbaabab

样例输出 #2

156
138
138
147
194

样例 #3

样例输入 #3

见附件中的 string/string3.in

样例输出 #3

见附件中的 string/string3.ans

样例 #4

样例输入 #4

见附件中的 string/string4.in

样例输出 #4

见附件中的 string/string4.ans

提示

【样例 #1 解释】

对于第一组数据,所有的方案为

  1. A = n A=\texttt{n} A=n B = nr B=\texttt{nr} B=nr C = nnr C=\texttt{nnr} C=nnr
  2. A = n A=\texttt{n} A=n B = nrn B=\texttt{nrn} B=nrn C = nr C=\texttt{nr} C=nr
  3. A = n A=\texttt{n} A=n B = nrnn B=\texttt{nrnn} B=nrnn C = r C=\texttt{r} C=r
  4. A = nn A=\texttt{nn} A=nn B = r B=\texttt{r} B=r C = nnr C=\texttt{nnr} C=nnr
  5. A = nn A=\texttt{nn} A=nn B = rn B=\texttt{rn} B=rn C = nr C=\texttt{nr} C=nr
  6. A = nn A=\texttt{nn} A=nn B = rnn B=\texttt{rnn} B=rnn C = r C=\texttt{r} C=r
  7. A = nnr A=\texttt{nnr} A=nnr B = n B=\texttt{n} B=n C = nr C=\texttt{nr} C=nr
  8. A = nnr A=\texttt{nnr} A=nnr B = nn B=\texttt{nn} B=nn C = r C=\texttt{r} C=r

【数据范围】

测试点编号 ∣ S ∣ ≤ \lvert S \rvert \le S特殊性质
1 ∼ 4 1 \sim 4 14 10 10 10
5 ∼ 8 5 \sim 8 58 100 100 100
9 ∼ 12 9 \sim 12 912 1000 1000 1000
13 ∼ 14 13 \sim 14 1314 2 15 2^{15} 215 S S S 中只包含一种字符
15 ∼ 17 15 \sim 17 1517 2 16 2^{16} 216 S S S 中只包含两种字符
18 ∼ 21 18 \sim 21 1821 2 17 2^{17} 217
22 ∼ 25 22 \sim 25 2225 2 20 2^{20} 220

对于所有测试点,保证 1 ≤ T ≤ 5 1 \le T \le 5 1T5 1 ≤ ∣ S ∣ ≤ 2 20 1 \le |S| \le 2^{20} 1S220

我有个梦想,我想写一篇多图的,讲解细的,注释多的,O(nlog26)O(nlog26)O(nlog26)复杂度的题解。

前置知识

  1. 树状数组 这个相信能参加NOIP的同学都会。
  2. 扩展KMP算法 模板题:P5410 【模板】扩展 KMP(Z 函数) 这道题我也写了题解,欢迎评论点赞围观。这道模板题是紫色的,其实一点也不难,我觉得难度有点虚标了,相信你看了我的题解很快就可以学会的。

如果你不会扩展KMP算法,其实也不影响你阅读本文,只需知道这个算法提供了一个z函数的计算方式。

不妨设输入的字符串是sss,下标从000开始,长度是nnn。用s[i,j]s[i,j]s[i,j]表示sss中从下标iii位置到下标jjj位置的子串,包括iiijjj。那么z[i]z[i]z[i]表示s[i,n−1]s[i,n-1]s[i,n1]sss本身的最长公共前缀的长度。

比如z[0]z[0]z[0]表示ssssss的公共子串的长度,那么z[0]=nz[0]=nz[0]=n

z[1]z[1]z[1]表示sss去掉开头第一个字符以后,和自己的最长公共前缀的长度,依次类推。扩展KMP算法可以在O(n)O(n)O(n)时间内求出zzz数组的所有位置。

枚举循环节长度

先不考虑题中的出现奇数次这个条件。先考虑把sss拆成(AB)kC(AB)^kC(AB)kC的方案数,其中ABABAB合起来是一个循环节,我们先考虑一个循环节有多长。

不妨设循环节长度为iii,第一个发现是,只要满足2≤i≤n−12 \leq i \leq n-12in1都可以。因为AAABBB都不能为空,所以循环节长度至少是222。循环节长度最多是n−1n-1n1,因为CCC也不能为空。

当循环节长度是iii的时候,至少kkk可以取到111,就是只循环一次,后面剩下的都给CCC。除此之外,k还可以取多少呢?不妨以i=3i=3i=3为例,画一个图看看

假设z[3]=7z[3]=7z[3]=7,也就是说,字符串从333位置开始,连续777个长度的子串,和字符串刚开头的777个字符相等。那么我们把sss再画一遍,去掉前333个位置,抄在下面,如图所示,那么画蓝线的部分是相等的。

现在考虑长度为333的循环节,首先红色的333个位置,和下面橙色的三个位置肯定是相等的,因为它们都是蓝线部分开头的333个。而橙色的部分上下是对应相等的,于是红色和橙色相等。

同理,由于橙色和绿色相等,那么红,橙,绿都相等,也就是说长度为3的循环节,至少可以循环3次。这里可以看出,蓝线的长度,除以循环节的长度再加1,就可以算出来最多可以循环几次,如果不能整除,向下取整。这个例子里面就是⌊7/3⌋+1\lfloor 7/3 \rfloor +17/3+1,那么kkk可以取333种。

那么第一个结论就出来了,枚举一下循环节的长度i,总的方案数就是

⌊z[i]i⌋+1\lfloor \frac{z[i]}{i} \rfloor +1 iz[i]+1

之和。

考虑字符出现的次数

题目中还有一个条件我们没有考虑,为了说话方便,我们定义f(i,j)f(i,j)f(i,j)表示s[i,j]s[i,j]s[i,j]中出现奇数次的字符的个数。

假设现在枚举到循环节长度为iii,此时根据前面的结论,我们算出来kkk的方案数有ttt种。其中一半kkk是奇数,一半kkk是偶数。当然奇数可能比偶数多一个。不妨设kkk是奇数的取值方案数是tjtjtj,那么有tj=t−t/2tj = t-t/2tj=tt/2,同理设偶数的kkk的个数是tototo,有to=t/2to = t/2to=t/2

考虑kkk的取值。如果kkk的值是奇数:

如图所示,k=1k=1k=1时候,CCC的长度是从iii开头的后缀的长度。k=3k=3k=3的时候,CCC是更短的红线表示的范围。这两种情况下,CCC当中出现次数为奇数的字符的个数是一样多的,因为如果循环节多出现了两次,相当于循环节里面的每个字符都出现了偶数次,不影响CCC当中出现次数为奇数次的字符的个数。

所以,当kkk是奇数的时候,我们只需计算出来,以iii为开头的后缀当中出现奇数次的字符的个数就行了。然后在长度为i的子串的前缀s[0,j]s[0,j]s[0,j]当中,找出来满足:

f(0,j)≤f(i,n−1)f(0,j) \leq f(i,n-1) f(0,j)f(i,n1)

jjj的个数,记为t1t1t1。而对于每个合法的jjj,我们发现,只要是奇数的kkk,都可以是一种合法方案,所以这里要乘法原理一下,把t1t1t1tjtjtj的个数相乘。

再来考虑k是偶数的情况。

跟前面情况类似,当k是偶数的时候,后缀C里面的只出现奇数次的字符的个数,和整个串里面只出现奇数次的字符个数是相等的。在长度为i的子串的前缀s[0,j]s[0,j]s[0,j]当中,找出来满足:

f(0,j)≤f(0,n−1)f(0,j) \leq f(0,n-1) f(0,j)f(0,n1)

jjj的个数,记为t2t2t2。那么这种情况下的方案数,就是t2t2t2tototo相乘。

具体实现

首先前面的算法中用到了f(0,n−1)f(0,n-1)f(0,n1),这个好办,直接预处理算出来就行了。

在从左往右扫字符串sss的过程中,假设我们目前枚举到位置iii,此时我们计算循环节长度为i+1i+1i+1的情况,此时AAA最远可以取到i−1i-1i1位置,把iii位置留着给BBB,保证BBB不为空。我们可以用一个桶,维护i+1i+1i+1开头的后缀里面每个字母出现的次数,当iii向右循环的时候,每次只改一个字符,所以在桶的对应位置减111,然后看看出现奇数次的字符数量如何变化就行了。

不妨设这个桶叫afterafterafter,一共有262626个格,分别表示每个字符出现的次数。起始的时候,先扫一遍s,把整个字符串都统计一遍,这时候可以顺手求出整个串里面只出现奇数次的字符的个数,记到allallall变量里面。然后当后面循环的时候,循环到iii位置,就把after[s[i]−′a′]after[s[i]-'a']after[s[i]a]位置减111。设变量sufsufsuf表示f[i+1,n−1]f[i+1,n-1]f[i+1,n1],如果在after[s[i]−′a′]after[s[i]-'a']after[s[i]a]减1之前,sufsufsuf是奇数,那么减完以后,后缀里面的奇数就会少一个,那么sufsufsuf111,否则sufsufsuf加一。这样每次iii移动,sufsufsuf都可以O(1)O(1)O(1)维护。

那么循环节里面的每个前缀中出现奇数次的字符个数,可以放到一个树状数组里面,这样这个树状数组一共有262626个位置,每个位置ppp保存出现奇数次的字符有ppp个的前缀有多少个。iii每向右循环111位,就在计算完结果以后,把当前前缀扔到树状数组里面。

其他细节可以参考代码

代码时间

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = (1 << 20) + 5;
char s[MAXN];//输入的字符串
int n, z[MAXN];//字符串长度,以及z函数的值
int before[30], after[30];//两个桶,分别统计当前枚举到的位置左侧和右侧每个字符出现的频次
int pre, suf, all;//当前枚举到的位置的前缀、后缀、整个串里面出现奇数次的字符的个数
//树状数组,对于s的某个前缀si,如果它里面出现奇数次的字符有m个,则在树状数组m+1位置+1
int c[30];
inline int lbt(int x) {
    return x & -x;
}
void update(int x) {
    while (x <= 27) {
        c[x]++;
        x += lbt(x);
    }
}
int sum(int x) {
    int r = 0;
    while (x > 0) {
        r += c[x];
        x -= lbt(x);
    }
    return r;
}
//扩展KMP算法,计算z函数的值
//可以参考良心博客 https://www.luogu.com.cn/blog/nitubenben/solution-p5410
void Z() {
    z[0] = n;
    int now = 0;
    while (now + 1 < n && s[now] == s[now + 1]) {
        now++;
    }
    z[1] = now;
    int p0 = 1;
    for (int i = 2; i < n; ++i) {
        if (i + z[i - p0] < p0 + z[p0]) {
            z[i] = z[i - p0];
        } else {
            now = p0 + z[p0] - i;
            now = max(now, 0);
            while (now + i < n && s[now] == s[now + i]) {
                now++;
            }
            z[i] = now;
            p0 = i;
        }
    }
}
int main() {
    int T;
    cin >> T;
    while (T--) {
        cin >> s;
        n = strlen(s);
        memset(before, 0, sizeof(before));
        memset(after, 0, sizeof(after));
        memset(c, 0, sizeof(c));
        all = pre = suf = 0;
        Z();
        //如果发现循环节可以到结尾,减1,空至少一个位置给C
        for (int i = 0; i < n; ++i) {
            if (i + z[i] == n) z[i]--;
        }
        //先把字符串过一遍,频次统计到after数组里面
        for (int i = 0; i < n; ++i) {
            after[s[i] - 'a']++;
        }
        //扫一下每个字母,计算整个串中出现奇数次的字符的个数
        for (int i = 0; i < 26; ++i) {
            if (after[i] & 1) {
                all++;
            }
        }
        suf = all;//后缀中的值暂时和整个串一致
        long long ans = 0;
        for (int i = 0; i < n; ++i) {
            //再扫一次字符串,当循环到i位置的时候,循环节长度是i+1
            //s[i]要从右边去掉,维护after数组和suf变量
            if (after[s[i] - 'a'] & 1) {
                //之前是奇数,现在变成偶数了
                suf--;
            } else {
                suf++;
            }
            after[s[i] - 'a']--;
            //s[i]加到左边,维护before和pre变量
            if (before[s[i] - 'a'] & 1) {
                pre--;
            } else {
                pre++;
            }
            before[s[i] - 'a']++;
            if (i != 0 && i != n - 1) {
                //循环节大于1,才能对答案有贡献,因为题中说ABC都不为空
                int t = z[i + 1] / (i + 1) + 1;
                ans += 1LL * (t / 2) * sum(all + 1) + 1LL * (t - t / 2) * sum(suf + 1);
            }
            update(pre + 1);
        }
        cout << ans << endl;
    }
    return 0;
}
  • 39
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
根据引用\[1\],对于问题P7114 \[NOIP2020\] 字符串匹配,我们可以使用枚举的方法来确定AB的可能情况,并进一步确定C的可能情况。假设枚举的AB是前缀iii,那么C只可能是后缀i+1i+1i+1,以及后缀i+1i+1i+1去掉前面的若干AB后得到的后缀。我们可以预处理出每个前缀和后缀中出现奇数次字符的个数。根据观察,对于AB循环奇数次和偶数次来说,它们的C的上述值分别相同。因此,我们可以分别考虑AB循环奇数次和偶数次的情况,从前往后记录当前有多少个前缀的出现奇数次字符个数为i。在询问时,我们可以直接暴力询问前缀和,总的复杂度大约是O(26n)。如果使用树状数组,复杂度可以优化到O(nlog26)。 根据引用\[2\],这个问题与周期串有关。朴素的KMP算法只能求出一个周期串的最小循环节,对于本题来说是不够的。我们需要知道对于一个AB来说,它最多能往后循环多少次。虽然可以使用二分法来解决,但是复杂度较高。 根据引用\[3\],我们可以优化复杂度。由于postodd每次只变化1,我们可以记录从i变化到i+1或者i-1时,A串满足条件的个数的变化情况。对于奇数k,没有什么特殊的情况。对于偶数k,我们注意到C串的奇偶性与整串一致,因此可以直接使用整串的奇偶性即可。 综上所述,对于问题P7114 \[NOIP2020\] 字符串匹配,我们可以使用枚举的方法来确定AB的可能情况,并根据不同情况进行处理。可以使用树状数组来优化复杂度。 #### 引用[.reference_title] - *1* *2* [【LuoguP7114】[NOIP2020] 字符串匹配 (扩展KMP算法)](https://blog.csdn.net/element_hero/article/details/113036427)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [P7114 [NOIP2020] 字符串匹配](https://blog.csdn.net/beneath_haruhi/article/details/120634165)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一只贴代码君(yaosicheng)

帅帅的你,留下你的支持吧

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

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

打赏作者

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

抵扣说明:

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

余额充值