P7114 [NOIP2020] 字符串匹配
Link:https://www.luogu.com.cn/problem/P7114
知识点:枚举,结论,Z 函数,哈希
唉,三年了,三年!!!
简述
组数据,每组数据给定仅由小写字母组成的字符串 ,求 的方案数,其中 ,其中 表示字符串 中出现奇数次的字符的数量。两种方案不同当且仅当拆分出的 、、 中有至少一个字符串不同。
对于所有测试点,保证 ,。
1S,512MB。
分析
考场垃圾做法
看到古老的代码了回忆一下。
首先预处理所有前缀和后缀中出现次数为奇数次的字符的数量,然后考虑枚举 的长度,再枚举 的长度,可以通过字符串哈希判断枚举的 是否合法。确定了 后 唯一确定,仅需求所有 的可行值中满足 的数量,可通过树状数组维护 求得。
枚举 的时间复杂度是调和级数,总时间复杂度为 。
考场上写丑了清空居然全都是暴力 memset 并且树状数组值域开到了 、、、其实把 memeset 换成枚举并且把树状数组值域改小可以概率卡过去。考场上写了什么几把啊我草幸亏没被卡多测清空好歹水到 84 分不然省一没了呜呜
真算法
同样考虑枚举 ,但是接下来不暴力枚举 而是进一步深挖性质。
首先考虑以 为循环节的最长前缀可以到什么位置,即求合法的 的 的最大值。手玩了下发现可以通过求 与 的最长公共前缀,也即 Z 函数求得,满足:
注意 都要求非空,所以上式中有一个取 防止出现 非空的情况。
然后考虑为什么要强调 代表出现次数为奇数的字符数量?手玩下发现 中所有字符出现次数均为偶数,也即当 的变化量为偶数时 是不变的,仅需讨论 为奇数与偶数的情况即可确定 ,再求有多少 合法即可,不需要再枚举 了。
为奇数的个数为 ,此时有 ; 为偶数的个数为 ,此时有 。同样可通过树状数组维护 求合法 的数量。
比考场做法少了枚举 的调和级数,总时间复杂度为 级别。
已经有了不依赖于字符集大小纯线性的做法?牛逼,但是懒了。
代码
真算法:
复制复制// /* By:Luckyblock */ #include <bits/stdc++.h> #define LL long long const int kN = 1e6 + 5e5 + 10; const int kM = 30; //============================================================= int n, z[kN]; int cnt_pre[30], cnt_suf[30], sum_pre[kN], sum_suf[kN]; char s[kN]; LL ans; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); return f * w; } namespace Bit { #define lowbit(x) ((x)&-(x)) int time, t[kM], tim[kM]; void Init() { ++ time; } void Insert(int pos_) { ++ pos_; for (int i = pos_; i <= 27; i += lowbit(i)) { if (tim[i] != time) t[i] = 0, tim[i] = time; t[i] ++; } } int Sum(int pos_) { ++ pos_; int ret = 0; for (int i = pos_; i; i -= lowbit(i)) { if (tim[i] != time) t[i] = 0, tim[i] = time; ret += t[i]; } return ret; } #undef lowbit } void z_function() { z[1] = n; for (int i = 2, l = 1, r = 1; i <= n; ++ i) { if (i <= r && z[i - l + 1] < r - i + 1) { z[i] = z[i - l + 1]; } else { z[i] = std::max(0, r - i + 1); while (i + z[i] <= n && s[z[i] + 1] == s[i + z[i]]) ++ z[i]; } if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1; } } void Init() { ans = 0; scanf("%s", s + 1); n = strlen(s + 1); for (int i = 0; i <= 27; ++ i) cnt_pre[i] = cnt_suf[i] = 0; for (int i = 1; i <= n + 1; ++ i) sum_pre[i] = sum_suf[i] = 0; for (int i = 1; i <= n; ++ i) { ++ cnt_pre[s[i] - 'a']; if (cnt_pre[s[i] - 'a'] % 2 == 1) sum_pre[i] = sum_pre[i - 1] + 1; else sum_pre[i] = sum_pre[i - 1] - 1; } for (int i = n; i >= 1; -- i) { ++ cnt_suf[s[i] - 'a']; if (cnt_suf[s[i] - 'a'] % 2 == 1) sum_suf[i] = sum_suf[i + 1] + 1; else sum_suf[i] = sum_suf[i + 1] - 1; } z_function(); Bit::Init(); } void Solve() { for (int ab = 2; ab <= n; ++ ab) { int cnt = std::min(z[ab + 1] / ab + 1, (n - 1) / ab); Bit::Insert(sum_pre[ab - 1]); ans += 1ll * (cnt + 1) / 2ll * Bit::Sum(sum_suf[ab + 1]); ans += 1ll * cnt / 2ll * Bit::Sum(sum_suf[1]); } } //============================================================= int main() { // freopen("1.txt", "r", stdin); int T = read(); while (T --) { Init(); Solve(); printf("%lld\n", ans); } return 0; } /* 1 aaaaaa 1 aaaa */
考场垃圾做法:
以下为考场代码。
// /* By:Luckyblock fuck ccf //freopen 我知道会有人看见的。 这里是一个已退役选手,在结束前 10 分钟的随想。 近 2 年的喜怒哀乐,在脑中缓缓流淌。 我曾妄想:挥斥方遒,信步走出考场—— 终不如愿, 别了, 我愿为你鼓掌。 Goodbye,World! */ #include <algorithm> #include <cctype> #include <cstdio> #include <cstring> #define LL long long const int kN = 1e6 + 4e5 + 10; const int mod1 = 998244353; const int mod2 = 1e9 + 7; const int kBase = 23333; //============================================================= int n, cnt_pre[30], cnt_suf[30], sum_pre[kN], sum_suf[kN]; int pw1[kN], pw2[kN], has1[kN], has2[kN]; LL ans; char s[kN]; //============================================================= int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) { if (ch == '-') f = -1; } for (; isdigit(ch); ch = getchar()) { w = 10 * w + ch - '0'; } return f * w; } namespace Bit { #define low(x) (x&-x) int t[kN]; void Init() { memset(t, 0, sizeof (t)); } void Insert(int pos_) { for (int i = pos_; i <= n; i += low(i)) { t[i] ++; } } int Sum(int pos_) { int ret = 0; for (int i = pos_; i; i -= low(i)) { ret += t[i]; } return ret; } #undef low } void Init() { ans = 0; Bit::Init(); memset(cnt_pre, 0, sizeof (cnt_pre)); memset(cnt_suf, 0, sizeof (cnt_suf)); } bool Judge(int r1_, int l2_, int r2_) { int h1 = (1ll * (1ll * has1[r2_] - 1ll * has1[l2_ - 1] * pw1[r2_ - l2_ + 1] % mod1) % mod1 + mod1) % mod1; int h2 = (1ll * (1ll * has2[r2_] - 1ll * has2[l2_ - 1] * pw2[r2_ - l2_ + 1] % mod2) % mod2 + mod2) % mod2; return ((h1 == has1[r1_]) && (h2 == has2[r1_])); } void Prepare() { Init(); scanf("%s", s + 1); n = strlen(s + 1); for (int i = 1; i <= n; ++ i) { int now = s[i] - 'a'; cnt_pre[now] ++; if (cnt_pre[now] % 2 == 1) { sum_pre[i] = sum_pre[i - 1] + 1; } else { sum_pre[i] = sum_pre[i - 1] - 1; } has1[i] = 1ll * (1ll * has1[i - 1] * kBase % mod1 + now) % mod1; has2[i] = 1ll * (1ll * has2[i - 1] * kBase % mod2 + now) % mod2; } for (int i = n; i; -- i) { int now = s[i] - 'a'; cnt_suf[now] ++; if (cnt_suf[now] % 2 == 1) { sum_suf[i] = sum_suf[i + 1] + 1; } else { sum_suf[i] = sum_suf[i + 1] - 1; } } } //============================================================= int main() { int T = read(); pw1[0] = pw2[0] = 1; for (int i = 1; i <= kN - 10; ++ i) { pw1[i] = 1ll * pw1[i - 1] * kBase % mod1; pw2[i] = 1ll * pw2[i - 1] * kBase % mod2; } while (T --) { Prepare(); for (int ab = 2; ab < n; ++ ab) { Bit::Insert(sum_pre[ab - 1] + 1); ans += 1ll * Bit::Sum(sum_suf[ab + 1] + 1); for (int c = 2 * ab; c + 1 <= n; c += ab) { if (! Judge(ab, c - ab + 1, c)) break; ans += 1ll * Bit::Sum(sum_suf[c + 1] + 1); } } printf("%lld\n", ans); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效