Main-Lorentz 算法
给定一个长度为
我们将一个字符串连续写两遍所产生的新字符串称为 重串 (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) - Fibonacci 字符串定义如下:
可以发现 Fibonacci 字符串具有高度的周期性。对于长度为
Main-Lorentz 算法¶
Main-Lorentz 算法的核心思想是 分治。
这个算法将字符串分为左、右两部分,首先计算完全处于字符串左部(或右部)的重串数量,然后计算起始位置在左部,终止在右部的重串数量。(下文中,我们将这种重串称为 交叉重串)
计算交叉重串的数量是 Main-Lorentz 算法的关键点,我们将在下文详细探讨。
寻找交叉重串¶
我们记某字符串的左部为
对于任意一个重串,我们考虑其中间字符。此处我们将一个重串右半边的第一个字符称为其中间字符,换言之,若
接下来,我们将会探讨如何找到所有的左偏重串。
我们记一个左边重串的长度为
我们考虑固定
显然,我们一旦固定了
左偏重串的判定¶
即使固定
我们再来举一个例子,对于字符串
于是,我们可以给出某个长度为
记
总结一下,即有:
- 固定一个
\textit{cntr} - 那么我们此时要找的重串长度均为
2l = 2(|u| - \textit{cntr}) l_1 l_2 - 计算上文提到的
k_1 k_2 - 则所有符合条件的重串符合条件:
接下来,只需要考虑如何快速算出
- 计算
k_1 \overline{u} - 计算
k_2 v + \# + u \# u v
右偏重串¶
计算右偏重串的方法与计算左偏重串的方法几乎一致。考虑该重串第一个落入
令
枚举
算法实现¶
Main-Lorentz 算法以四元组
请注意,如果你想通过这些四元组来找到所有重串的起始位置与终止位置,则最坏时间复杂度会达到 repetitions
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | 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);
}
}
|
本页面主要译自博文 Поиск всех тандемных повторов в строке. Алгоритм Мейна-Лоренца 与其英文翻译版 Finding repetitions。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:OI-wiki
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用