[NOIP2020] 字符串匹配
[NOIP2020] 字符串匹配
去年的我题都看错了, 看对了之后也完全没有头绪, 今年我虽然还是菜的一批, 但是能自己做做了.
虽然一看到的时候仍然没有任何头绪, 然后就开始写, 写自己能想到的所有东西, 在纸上画, 画这个串, 手玩, 发现一些性质.
心路历程
首先, 我们忽视 \(F(A) \le F(C)\) 这个条件, 那么 \(C\) 的选择就有 \(n - 2\) 种, \(|(AB)^i| \in [2, n - 1]\) .
那么我们需要考虑的就是 \(AB\) 该怎么选.
我们设去掉 \(C\) 的串长为 \(m\) , \(|AB|\) 为 \(l\) , 那么 \(l | m\) . 我们就只需要求 \(AB\) 是否是剩下的一段的循环节就行了, 而 \(m\) 的质因子最多有 \(\log m\) 个, 我们有 \(n\) 个 \(m\) , 每个 \(m\) 有 \(\log m\) 个质因子, 所以我们只需要判断 \(n \log m\) 次 \(AB\) .
接下来考虑如何判断 \(AB\) 是不是循环节.
这里我们有一个结论, 这里定义三个串, \(A, B, C\) , 这里的 \(ABC\) 跟上面无关. \(|ABC| \mod\ |A| == 0\) , \(AB == BC\) , 那么 \(A\) 就是 \(ABC\) 的一个循环节.
由上面的结论, 我们可以预处理出原串的 \(Hash\) 然后 \(O(1)\) 判断, 如果 \((AB)^i\) 是合法的, 那么 \((aAB)^{i/a}\) 也合法. 所以 \(AB\) 的贡献就是 \(|AB| - 1 + 2|AB| - 1 + ... + i|AB| - 1\) , 也就是 \((1 + i)i / 2 * |AB| - i\) . (或者先打标记, 然后统一统计, 这样应该会好写很多我感觉, 有点类似线性筛的感觉) .
这样我们就用 \(n \log m\) 的时间把对于每个 \(C\) 的所有可以作为循环节的长度求出来了, 接下来考虑 \(F(A) \le F(C)\) 这个限制.
\(F(S)\) 是 \(S\) 中出现奇数次的字符的数量, 考虑一下有没有什么性质.
由于 \(A\) 一定是前缀, \(C\) 一定是后缀, 那么我们就可以正反分别扫一遍, 记录每个前缀和后缀的出现奇数次的字符数, 复杂度 \(O(n)\) , 完全可行.
话不多说, 开干!
正文
其实我在写代码的时候发现我上面的思路其实是有问题的, 但是仍然提供了非常多的可行之处, 主要是各种性质, 使得我们可以在很短的时间内进行判断, 但是我没有找到优化枚举的手段.
我这里没有找到任何优化枚举的手段, 所以只能给 \(O(n \log n)\) 枚举, 枚举 \(AB\) 的长度以及重复的次数.
因为我没有优化枚举的手段, 那么我们就让 \(Judge\) 尽可能的快.
我先打了个暴力, 用 \(Hash\) \(O(1)\) 判断 \(AB\) 是否是循环节以及 \(F(A), F(C)\) .
\(code:\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * f;
}
const int N = 1 << 21, bs = 27;
struct sovle {
int n, cnt1[N], cnt2[N], tot[30];
ull hs[N], pw[N];
char s[N];
void get_hash() {
pw[0] = 1;
for (int i = 1; i <= n; i++) {
hs[i] = hs[i - 1] * bs + s[i] - 'a' + 1;
pw[i] = pw[i - 1] * bs;
}
}
void get_cnt() {
memset(cnt1, 0, sizeof(cnt1));
memset(cnt2, 0, sizeof(cnt1));
memset(tot, 0, sizeof(tot));
for (int i = 1; i <= n; i++) {
int j = s[i] - 'a';
tot[j]++;
cnt1[i] = cnt1[i - 1] + (tot[j] & 1 ? 1 : -1);
}
memset(tot, 0, sizeof(tot));
for (int i = n; i >= 1; i--) {
int j = s[i] - 'a';
tot[j]++;
cnt2[i] = cnt2[i + 1] + (tot[j] & 1 ? 1 : -1);
}
}
inline ull get(int l, int r) {
return hs[r] - hs[l - 1] * pw[r - l + 1];
}
void run() {
scanf("%s", s + 1);
n = strlen(s + 1);
ll ans = 0;
get_hash();
get_cnt();
for (int i = 2; i < n; i++) {
for (int j = i; j < n; j += i) {
if (get(1, i) == get(j - i + 1, j)) {
for (int k = 1; k < i; k++) {
if (cnt1[k] <= cnt2[j + 1]) ans++;
}
}
else break;
}
}
printf("%lld\n", ans);
}
} S;
int main() {
int n = read();
for (int i = 1; i <= n; i++) S.run();
return 0;
}
\(48pts\)
复杂度 \(O(TN^2 \log N)\) .
然后我们发现, 我们其实不必要枚举 \(A\) 的长度, 因为我们只要 \(F(A) \le F(C)\) 就合法了, 所以我们可以用一个前缀和优化, 记 \(cnt[i][k]\) 表示长度为 \(i\) 的前缀中 \(F(A) \le k\) 的 \(A\) 的个数, 这样我们 \(O(26N)\) 预处理, \(O(1)\) 查询, 总复杂度 \(O(26TN + TN \log N)\)
\(code:\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * f;
}
const int N = 1 << 21, bs = 27;
struct sovle {
int n, cnt1[N], cnt2[N], cnt[N][27], tot[30];
ull hs[N], pw[N];
char s[N];
void get_hash() {
pw[0] = 1;
for (int i = 1; i <= n; i++) {
hs[i] = hs[i - 1] * bs + s[i] - 'a' + 1;
pw[i] = pw[i - 1] * bs;
}
}
void get_cnt() {
memset(cnt1, 0, sizeof(cnt1));
memset(cnt2, 0, sizeof(cnt1));
memset(tot, 0, sizeof(tot));
for (int i = 1; i <= n; i++) {
int j = s[i] - 'a';
tot[j]++;
cnt1[i] = cnt1[i - 1] + (tot[j] & 1 ? 1 : -1);
for (int k = 0; k < cnt1[i]; k++) cnt[i][k] = cnt[i - 1][k];
for (int k = cnt1[i]; k < 27; k++) cnt[i][k] = cnt[i - 1][k] + 1;
}
memset(tot, 0, sizeof(tot));
for (int i = n; i >= 1; i--) {
int j = s[i] - 'a';
tot[j]++;
cnt2[i] = cnt2[i + 1] + (tot[j] & 1 ? 1 : -1);
}
}
inline ull get(int l, int r) {
return hs[r] - hs[l - 1] * pw[r - l + 1];
}
void run() {
scanf("%s", s + 1);
n = strlen(s + 1);
ll ans = 0;
get_hash();
get_cnt();
for (int i = 2; i < n; i++) {
for (int j = i; j < n; j += i) {
if (get(1, i) == get(j - i + 1, j)) {
ans += cnt[i - 1][cnt2[j + 1]];
}
else break;
}
}
printf("%lld\n", ans);
}
} S;
int main() {
int n = read();
for (int i = 1; i <= n; i++) S.run();
return 0;
}
\(84pts\)
然后 \(O2\) 一开, 谁也不爱(bushi
这样其实就差不多了 \(qwq\) , 比去年的我已经强太多了, 在基于我的能力下, \(84pts\) 所花费的时间与分数的比是最优解, 现在最重要的已经不是切题了, 而是在最短的时间内拿最多的分, 不过正解思路还是要有的.