标签:左右 数组 sub 字符串长度 镜像 tier content 下标 border
利用回文串的「镜像」特点减少计算。
引理 0
设 $S$ 是一个长度为 $n+1$ 回文串,下标从 $0$ 开始;$T = S[l, r]$ 是 $S$ 的子串。$T$ 是回文串当且仅当 $S[n-r, n-l]$ 是回文串。
先考虑长度为奇数的回文子串(简称为「奇回文子串」),可以求出以每个下标为中心的最长奇回文子串的长度。
用 $P_i$ 表示以下标 $i$ 为中心的最长回文子串,用 $f(i)$ 表示 $P_i$ 的长度,即 $f(i) = |P_i|$ 。
用 $L_i, R_i$ 分别表示 $P_i$ 的左半部分和右半部分。用 $l_i,r_i$ 分别表示 $P_i$ 的左右端点的下标。
引理 1
设 $i$,$j$ 是两个下标。
(1) 若 $j > i$ 且 $r_j \le r_i $ 则 $ P_j \subseteq P_i $;
(2) 若 $j < i$ 且 $r_j \le r_i $ 则 $R_j \subseteq P_i$ 。
试着观察这种做法中的冗余计算。
字符串下标从 0 开始,$S[0, n)$ 。
假设当前以第 $i$ 为重心,考虑在 $i$ 之前是否有「镜子」$j$ 使得 $i$ 对着 $j$ 能照出自己。换言之是否存在 $j<i$ 使得以 $j$ 为中心的回文子串能够「波及」$i$ 。用式子表示就是 $f(j) \ge 2(i-j) + 1 $ 或者 $j + f(j)/2 \ge i$。$i$ 关于 $j$ 的镜像为 $2j -i$。
我们希望 $j$ 不仅能「照出」$i$ 还要能「照出」$i$ 右边尽量多的字符,换言之「照得尽量远」。用式子表示就是 $ j + f(j)/2$ 尽可能大。
回文串在镜像操作下保持不变。
若 $j + f(j)/2 \ge i$ 那么区间 $[j-f(j)/2, j+f(j)/2]$ 中以 $i$ 为中心的最长回文子串即区间$[j - f(j)/2 , j + f(j)/2]$ 中以 $2j-i$ 为中心的最长回文子串。
这个性质我一直绕不过来。
示意图
字符串的镜像操作相当于序列反转(reverse),因此回文串在镜像操作下保持不变。
$S[j-f(j)/2, j+f(j)/2]$ 中的任意子串都有一个关于 $j$ 的镜像串(即反转串)。
观察上图。
可 $O(1)$ 地计算出 $|P_i \bigcap P_j|$ ;若 $P_i \bigcap P_j$ 能向两侧扩展,那么右边界(border, frontier)将增大;右边界是单调不减的,因此扩展的总复杂度为 $O(n)$ 。于是 Manacher 算法的复杂度为 $O(n)$ 。
void manacher (char str [], int h[], int n) {
int m = 0;
static char buf[M]; // M是字符串最大长度的两倍。
//以'#'开头
for (int i = 0; i < n; ++i){
buf[m++] = '#', buf[m++] = str[i];
}
buf[m++] = '#'; // 以'#'结尾
buf[m] = '\0'; // 必须补零!
// r:右边界,mid:与r对应的中点;
for (int i = 0, r = 0, mid = 0; i < m; ++i) { // mid 不必初始化,可随意赋一个初值
h[i] = i < r ? std::min(r - i, h[2 * mid - i]) : 0;
while (h[i] <= i && buf[i - h[i]] == buf[i + h[i]])
++h[i];
if (r < i + h[i]) r = i + h[i], mid = i;
}
}
加了一个也许有点用的小小的优化
void manacher (char str [], int h[], int n) {
int m = 0;
static char buf[M]; // M是字符串最大长度的两倍。
//以'#'开头
for (int i = 0; i < n; ++i){
buf[m++] = '#', buf[m++] = str[i];
}
buf[m++] = '#'; // 以'#'结尾
buf[m] = '\0'; // 必须补零!
// r:右边界,mid:与r对应的中点;
for (int i = 0, r = 0, mid = 0; i < m; ++i) { // mid 不必初始化,可随意赋一个初值
h[i] = i < r ? std::min(r - i, h[2*mid - i]) : 0;
if(h[i] == r - i) { // 一个小小的优化
while (h[i] <= i && buf[i - h[i]] == buf[i + h[i]])
++h[i];
if (r < i + h[i]) r = i + h[i], mid = i;
}
}
}
将原字符串长度记为 $n$,则数组 $h$ 长为 $2n+1$ 。
原字符串的最长回文子串的长度即 $h$ 的最大值减 $1$ 。
对于 $ 0 \le i \le 2n $,当 $i$ 为偶数时 $h[i] - 1$ 表示原字符串中「右半边起点的下标为 $i/2$」的最长偶回文串的长度;当 $i$ 为奇数时 $h[i] - 1$ 表示原字符串中「中心点下标为 $\lfloor i/2 \rfloor$」的最长奇回文串的长度。
标签:左右 数组 sub 字符串长度 镜像 tier content 下标 border
原文地址:https://www.cnblogs.com/Patt/p/9425615.html