Problem
Description
如果一个字符串可以被拆分为 \(AABB\) 的形式,其中 \(A\) 和 \(B\) 是任意非空字符串,则我们称该字符串的这种拆分是优秀的。
例如,对于字符串 aabaabaa,如果令 \(A = \mathrm{aab}\),\(B = \mathrm{a}\),我们就找到了这个字符串拆分成 \(AABB\) 的一种方式。
一个字符串可能没有优秀的拆分,也可能存在不止一种优秀的拆分。比如我们令 \(A=\mathrm{a}\),\(B=\mathrm{baa}\),也可以用 \(AABB\) 表示出上述字符串;但是,字符串 abaabaa 就没有优秀的拆分。
现在给出一个长度为 \(n\) 的字符串 \(S\),我们需要求出,在它所有子串的所有拆分方式中,优秀拆分的总个数。这里的子串是指字符串中连续的一段。
以下事项需要注意:
- 出现在不同位置的相同子串,我们认为是不同的子串,它们的优秀拆分均会被记入答案。
- 在一个拆分中,允许出现 \(A=B\)。例如 cccc 存在拆分 \(A=B=\mathtt{c}\)。
- 字符串本身也是它的一个子串。
Input Format
每个输入文件包含多组数据。输入文件的第一行只有一个整数 \(T\),表示数据的组数。保证 \(1 \le T \le 10\)。
接下来 \(T\) 行,每行包含一个仅由英文小写字母构成的字符串 \(S\),意义如题所述。
Output Format
输出 \(T\) 行,每行包含一个整数,表示字符串 \(S\) 所有子串的所有拆分中,总共有多少个是优秀的拆分。
Sample
input
4
aabbbb
cccccc
aabaabaabaa
bbaabaababaaba
output
3
5
4
7
Explanation
Explanation for Input
我们用 \(S[i, j]\) 表示字符串 \(S\) 第 \(i\) 个字符到第 \(j\) 个字符的子串(从 \(1\) 开始计数)。
第一组数据中,共有 \(3\) 个子串存在优秀的拆分:
\(S[1,4] = \mathtt{aabb}\),优秀的拆分为 \(A=\mathtt{a}\),\(B = \mathtt{b}\);
\(S[3,6] = \mathtt{bbbb}\),优秀的拆分为 \(A=\mathtt{b}\),\(B = \mathtt{b}\);
\(S[1,6] = \mathtt{aabbbb}\),优秀的拆分为 \(A= \mathtt{a}\),\(B = \mathtt{bb}\)。
而剩下的子串不存在优秀的拆分,所以第一组数据的答案是 \(3\)。
第二组数据中,有两类,总共 \(4\) 个子串存在优秀的拆分:
对于子串 \(S[1,4] = S[2,5] = S[3,6] = \mathrm{cccc}\),它们优秀的拆分相同,均为 \(A = \mathrm{c}\),\(B = \mathrm{c}\),但由于这些子串位置不同,因此要计算 \(3\) 次;
对于子串 \(S[1,6] = \mathrm{cccccc}\),它优秀的拆分有 \(2\) 种:\(A = \mathrm{c}\),\(B = \mathrm{cc}\) 和 \(A = \mathrm{cc}\),\(B = \mathrm{c}\),它们是相同子串的不同拆分,也都要计入答案。
所以第二组数据的答案是 \(3 + 2 = 5\)。
第三组数据中,\(S[1,8]\) 和 \(S[4,11]\) 各有 \(2\) 种优秀的拆分,其中 \(S[1,8]\) 是问题描述中的例子,所以答案是 \(2 + 2 = 4\)。
第四组数据中,\(S[1,4]\),\(S[6,11]\),\(S[7,12]\),\(S[2,11]\),\(S[1,8]\) 各有 \(1\) 种优秀的拆分,\(S[3,14]\) 有 \(2\) 种优秀的拆分,所以答案是 \(5 + 2 = 7\)。
Range
对于全部的测试点,保证 \(1 \le T \le 10\)。以下对数据的限制均是对于单组输入数据而言的,也就是说同一个测试点下的 \(T\) 组数据均满足限制条件。
我们假定 \(n\) 为字符串 \(S\) 的长度,每个测试点的详细数据范围见下表:
测试点编号 | \(n\) | 其他约束 |
---|---|---|
1、2 | \(\leq 300\) | \(S\)中所有字符全部相同 |
3、4 | \(\leq 2000\) | |
5、6 | \(\leq 10\) | 无 |
7、8 | \(\leq 20\) | |
9、10 | \(\leq 30\) | |
11、12 | \(\leq 50\) | |
13、14 | \(\leq 100\) | |
15 | \(\leq 200\) | |
16 | \(\leq 300\) | |
17 | \(\leq 500\) | |
18 | \(\leq 1000\) | |
19 | \(\leq 2000\) | |
20 | \(\leq 30000\) |
Algorithm
后缀数组,差分。
Mentality
拿到题目先看看数据范围,嗯 \(......?\) 九十五分的 \(n^2\) 暴力算什么,那考场上谁会打正解, 看着就很显然啊,我们记录 \(a[i]\) 为以 \(i\) 结尾的形如 \(AA\) 的字符串的数量,\(b[i]\) 为以 \(i\) 开头的形如 \(AA\) 的字符串的数量,那么以 \(i\) 为 \(AA\) 与 \(BB\) 的分界线的答案不就是 \(a[i]*b[i+1]\) 吗 = =。
那 \(Ans=\sum a[i]*b[i+1]\) 就好了,计算每个 \(a[i]\) 的时候 \(O(n)\) 枚举 \(AA\) 串的起点或终点,利用 \(hash\) 来 \(O(1)\) 判断它能不能从中间分成相同的两个字符串就好。
\(n^2\) 有 \(95\) 分真的仙 \(......\)
那么我们接着考虑一下如何做 \(100\) 分。打表即可。
首先观察到,假设我们每隔 \(len\) 就标记一个点的话,若两个相邻的标记点之间的 后缀的最长公共前缀(\(LCP\))+前缀的最长公共后缀(\(LCS\)) \(\ge len\) 的话,就是如下情况:
当然,这里还有一个问题:如何求出 \(LCS\) 与 \(LCP\) 呢?其实很简单,我们来感性理解一下后缀数组中的 \(height\) 数组的含义。如果我们将所有后缀数组拿出来建一颗 \(Trie\) 树,那么 \(height\) 数组是不是可以理解为两个相邻叶子节点的 \(lca\) 的深度呢?那么根据 \(rmq\) 求 \(lca\) 的原理,两个后缀的最长公共前缀的长度,不就是它们之间的每两个叶子节点的 \(lca\) 中深度最小的那个,也就是夹在它们中间的 \(height\) 数组中最小的那个吗?
用 \(rmq\) 预处理即可 \(O(1)\) 查询两个后缀的最长公共前缀了。
继续来看上图,蓝色的即为 \(LCP\) ,红色部分为 \(LCS\) 。设它们的重合部分长度为 \(same\) ,对于这种情况,我们不难发现,对于 \(i-LCS\) 到 \(i-LCS+same\) 的位置,以它们为开头的长度为 \(2*len\) 的串必定为 \(AA\) 形式的串。
我觉得这甚至是不需要证明的,因为从中间分开必定相等,实在想不出来在纸上画一画就行。
那么同理,以 \(i-LCS+2*len-1\) 到 \(i-LCS+same+2*len-1\) 结尾的长度为 \(2*len\) 的串也必定为 \(AA\) 形式。
则我们对于相邻的两个距离为 \(len\) 的点,如果它们的 \(LCP+LCS\ge len\) ,那么对于 \(b[i-LCS]\) 至 \(b[i-LCS+same]\) 的位置,我们都加上 \(1\) ,对于 \(a[i-LCS+2*len-1]\) 至 \(a[i-LCS+same+2*len-1\) 的位置,我们也都加上一,这便是当前这两个距离为 \(len\) 的点对 \(a,b\) 数组的贡献了。
对于这个加上去的过程,我们可以使用差分。
那么答案就很明显可以直接统计了。
总结下来我们的步骤如下:
- 枚举 \(len\) ,每隔 \(len\) 便标记一个点,这个的复杂度请百度
调和级数
,复杂度为 \(nlog\) 。 - 差分数组更改,复杂度 \(O(1)\)
- 统计答案,复杂度 \(O(n)\)
则最后的时间复杂度为 \(O(nlog+n)=O(nlog)\) 。
完毕。
代码细节较多,建议稍稍看一下,不愧是当年的防 \(AK\) 题 \(QwQ\) 。
Code
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int p, T, n, M, cnt, tag[30001], sa[2][30001], rk[2][30001], h[2][30001],
tp[2][30001], rmq[2][30001][16], Log[40001], a[30002], b[30002];
long long ans;
char s[2][30002];
void Sort(int x) {
for (int i = 1; i <= M; i++) tag[i] = 0;
for (int i = 1; i <= n; i++) tag[rk[x][i]]++;
for (int i = 1; i <= M; i++) tag[i] += tag[i - 1];
for (int i = n; i >= 1; i--) sa[x][tag[rk[x][tp[x][i]]]--] = tp[x][i];
}
int find(int x, int l, int r) {
if (l > r) swap(l, r);
l++;
int L = Log[r - l + 1];
return min(rmq[x][l][L], rmq[x][r - (1 << L) + 1][L]);
}
int main() {
cin >> T;
int now = 1, num = -1, top = 2;
Log[1] = 0;
for (int i = 1; i <= 15; i++) {
now <<= 1, num++;
while (top <= now) Log[top++] = num;
} //预处理 log
while (T--) {
memset(a, 0, sizeof(a));
memset(b, 0, sizeof(b));
memset(rk, 0, sizeof(rk));
memset(tp, 0, sizeof(tp));
scanf("%s", s[0] + 1);
n = strlen(s[0] + 1);
for (int i = 1; i <= n; i++)
s[1][i] = s[0][n - i + 1]; //反过来方便处理前缀
M = 76;
for (int i = 1; i <= n; i++) rk[0][i] = s[0][i] - 'a' + 1, tp[0][i] = i;
for (int i = n; i >= 1; i--) rk[1][i] = s[1][i] - 'a' + 1, tp[1][i] = i;
Sort(0), Sort(1);
for (int l = 0; l < 2; l++) {
cnt = 0;
for (int len = 1; cnt < n; len <<= 1, M = cnt) {
cnt = 0;
for (int i = n - len + 1; i <= n; i++) tp[l][++cnt] = i;
for (int i = 1; i <= n; i++)
if (sa[l][i] > len) tp[l][++cnt] = sa[l][i] - len;
Sort(l);
swap(tp[l], rk[l]);
rk[l][sa[l][1]] = cnt = 1;
for (int i = 2; i <= n; i++)
rk[l][sa[l][i]] =
tp[l][sa[l][i]] == tp[l][sa[l][i - 1]] &&
tp[l][sa[l][i] + len] == tp[l][sa[l][i - 1] + len]
? cnt
: ++cnt;
}
} //后缀与前缀的排序
for (int l = 0; l < 2; l++) {
for (int i = 1, len = 0; i <= n; i++) {
if (len) len--;
int now = sa[l][rk[l][i] - 1];
while (s[l][now + len] == s[l][i + len]) len++;
rmq[l][rk[l][i]][0] = len;
}
for (int j = 1; j <= 15; j++)
for (int i = 1; i <= n; i++)
if (i + (1 << (j - 1)) <= n)
rmq[l][i][j] =
min(rmq[l][i][j - 1], rmq[l][i + (1 << (j - 1))][j - 1]);
} // height 数组与 rmq 预处理
for (int len = 1; len <= n / 2; len++)
for (int i = len; i + len <= n; i += len) {
int l = i, r = i + len;
int LCS = find(1, rk[1][n - r + 2], rk[1][n - l + 2]),
LCP = find(0, rk[0][l], rk[0][r]); //计算 LCS 与 LCP
LCS = min(LCS, len - 1),
LCP = min(
LCP,
len); //如果 LCS 或 LCP 过大,则会在其他点算重,所以和 len 取 min
if (LCS + LCP >= len) {
int same = LCS + LCP - len + 1;
b[l - LCS]++, b[l - LCS + same]--;
a[l - LCS + 2 * len - 1]++, a[l - LCS + same + 2 * len - 1]--;
} //差分数组更改
}
for (int i = 1; i <= n; i++)
a[i] += a[i - 1], b[i] += b[i - 1]; //计算差分数组真实值
ans = 0;
for (int i = 1; i < n; i++) ans += a[i] * b[i + 1]; //计算答案
cout << ans << endl;
}
}