给定一个长度为
我们将一个字符串连续写两遍所产生的新字符串称为 重串 (tandem repetition)。下文中,为了表述精准,我们将被重复的这个字符串称为原串。换言之,一个重串等价于一对下标
你的目标是找出给定的字符串
下文的算法由 Michael Main 和 Richard J. Lorentz 在 1982 年提出。
声明:
下文所有的字符串下标从 0 开始。
下文中,记
考虑字符串
$s[2 \dots 5] = \tt abab$ $s[3 \dots 6] = \tt baba$ $s[7 \dots 8] = \tt ee$
下面是另一个例子,考虑字符串
$s[0 \dots 5] = \tt abaaba$ $s[2 \dots 3] = \tt aa$
一个长度为
但这并不影响我们在
这里有一些关于重串数量的有趣结论:
- 如果一个重串的原串不是重串,则我们称这个重串为 本原重串 (primitive repetition)。可以证明,本原重串最多有
$O(n \log n)$ 个。 - 如果我们把一个重串用 Crochemore 三元组
$(i, p, r)$ 进行压缩,其中$i$ 是重串的起始位置,$p$ 是该重串某个循环节的长度(注意不是原串长度!),$r$ 为这个循环节重复的次数。则某字符串的所有重串可以被$O(n \log n)$ 个 Crochemore 三元组表示。 - Fibonacci 字符串定义如下:
可以发现 Fibonacci 字符串具有高度的周期性。对于长度为
Main-Lorentz 算法的核心思想是 分治。
这个算法将字符串分为左、右两部分,首先计算完全处于字符串左部(或右部)的重串数量,然后计算起始位置在左部,终止在右部的重串数量。(下文中,我们将这种重串称为 交叉重串)
计算交叉重串的数量是 Main-Lorentz 算法的关键点,我们将在下文详细探讨。
我们记某字符串的左部为
对于任意一个重串,我们考虑其中间字符。此处我们将一个重串右半边的第一个字符称为其中间字符,换言之,若
接下来,我们将会探讨如何找到所有的左偏重串。
我们记一个左边重串的长度为
我们考虑固定
显然,我们一旦固定了
即使固定
我们再来举一个例子,对于字符串
于是,我们可以给出某个长度为
记
总结一下,即有:
- 固定一个
$\textit{cntr}$ 。 - 那么我们此时要找的重串长度均为
$2l = 2(|u| - \textit{cntr})$ 。此时可能仍有多个符合条件的重串,取决于$l_1$ 与$l_2$ 的取值。 - 计算上文提到的
$k_1$ ,$k_2$。 - 则所有符合条件的重串符合条件:
接下来,只需要考虑如何快速算出
- 计算
$k_1$ :只需计算$\overline{u}$ 的 Z 函数即可。 - 计算
$k_2$ :只需计算$v + # + u$ 的 Z 函数即可,其中$#$ 是一个$u$ ,$v$ 中均没有的字符。
计算右偏重串的方法与计算左偏重串的方法几乎一致。考虑该重串第一个落入
令
枚举
Main-Lorentz 算法以四元组
请注意,如果你想通过这些四元组来找到所有重串的起始位置与终止位置,则最坏时间复杂度会达到 repetitions
中。
vector<int> z_function(string const& s) {
int n = s.size();
vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; i++) {
if (i <= r) z[i] = min(r - i + 1, z[i - l]);
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) z[i]++;
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
return z;
}
int get_z(vector<int> const& z, int i) {
if (0 <= i && i < (int)z.size())
return z[i];
else
return 0;
}
vector<pair<int, int>> repetitions;
void convert_to_repetitions(int shift, bool left, int cntr, int l, int k1,
int k2) {
for (int l1 = max(1, l - k2); l1 <= min(l, k1); l1++) {
if (left && l1 == l) break;
int l2 = l - l1;
int pos = shift + (left ? cntr - l1 : cntr - l - l1 + 1);
repetitions.emplace_back(pos, pos + 2 * l - 1);
}
}
void find_repetitions(string s, int shift = 0) {
int n = s.size();
if (n == 1) return;
int nu = n / 2;
int nv = n - nu;
string u = s.substr(0, nu);
string v = s.substr(nu);
string ru(u.rbegin(), u.rend());
string rv(v.rbegin(), v.rend());
find_repetitions(u, shift);
find_repetitions(v, shift + nu);
vector<int> z1 = z_function(ru);
vector<int> z2 = z_function(v + '#' + u);
vector<int> z3 = z_function(ru + '#' + rv);
vector<int> z4 = z_function(v);
for (int cntr = 0; cntr < n; cntr++) {
int l, k1, k2;
if (cntr < nu) {
l = nu - cntr;
k1 = get_z(z1, nu - cntr);
k2 = get_z(z2, nv + 1 + cntr);
} else {
l = cntr - nu + 1;
k1 = get_z(z3, nu + 1 + nv - 1 - (cntr - nu));
k2 = get_z(z4, (cntr - nu) + 1);
}
if (k1 + k2 >= l)
convert_to_repetitions(shift, cntr < nu, cntr, l, k1, k2);
}
}