字符串进阶算法小结

前言:

本文参考《信息学奥赛一本通·金牌导航》。

正文:

Manacher 算法:

Manacher 算法,经常被称作马拉车,可以以 (mathcal{O}(n)) 的时间复杂度求出字符串关于回文子串一类的问题。

介绍:

首先举个例子,设字符串 (s= exttt{bbdkd})

在这里面,有偶回文子串 ( exttt{bb})、奇回文子串 ( exttt{dkd}),在计算的过程中还要对奇偶问题进行讨论太过于麻烦,所以可以在每个字符之间加入特殊字符(首尾特殊字符可以作边界):

[s'= exttt{$#b#b#d#k#d#@} ]

再设 (R_i) 表示以第 (i) 位字符为中心的最长回文的半径。对应上面的 (s') 可以得到:

[R={1,2,3,2,1,2,1,4,1,2,1} ]

可以发现,以 (i) 为中心的最长回文子串长度就是 (R_i-1)

我们在求解 (R) 数组的过程中再设 (p,mx) 分别表示当前回文子串中心和回文子串的右端点。若正在计算 (R_i),令 (i) 关于 (p) 的对称点 (j=2p-i)。然后为了先给 (R_i) 定下界,进行分类讨论(结合图片思考):

  1. (mxleq i),因为恒有 (R_igeq 1),所以 (R_i) 下界是 (1)
  2. (mx-i>R_j)(j) 的左端点没有包裹住 (p) 的左端点(即以 (j) 为中心的回文子串被包含与 (p) 的)。因为是对称点,所以以 (i) 为中心的回文子串也一定被包含与 (p) 的,所以 (R_i) 下界为 (R_j)
  3. (mx-ileq R_j)(j) 的左端点包裹住了 (p) 的左端点(即以 (j) 为中心的回文子串不被包含与 (p) 的)。因为不被包含,所以不能保证在 (mx') 以前的字符和 (i) 的对应,所以下界是 (mx-i)

简单来说,定下界就是取 (min(mx-i,R_j))

定完下界后,再往两边枚举检测一遍。

由于向左右拓展成功必然会导致 (mx) 向右移,而 (mx) 向右移不超过 (n) 次,故向左右拓展操作的总时间复杂度是 (mathcal{O}(n))。枚举 (i) 也是(mathcal{O}(n)) 的,那么 Manacher 算法时间复杂度是 (mathcal{O}(n))

代码:

int p = 1, mx = 1;
for (int i = 1; i <= n; i++)
{
	R[i] = min (mx - i, R[2 * p - i]);
	while (s[i - R[i]] == s[i + R[i]]) ++R[i];
	if (i + a[i] > mx)
		mx = i + R[i], p = i;
}

例题:

【YBTOJ】不交回文串

后缀数组:

介绍:

注意:本文第 (i) 个后缀是指关于原字符串 (S) 的子串 (S_{icdots ext{len}})

后缀数组 (SA) 就是以此存储将 (s)(n) 个后缀从小到大排序后的数组。即:排名为 (i) 的后缀是哪一个。

名次数组 (Rank)(SA) 的互逆,它储存的是第 (i) 个后缀的排名。


求出后缀数组 (SA),一般使用倍增的方法:用倍增对每个字符开始的长度 (2^k) 的子字符串进行排序,得到 (Rank)(k)(0) 开始,每次加一,当 (2^k) 大于 (n) 就一定能比较出大小。每次排序利用上一次长度为 (2^{k-1}) 的两个字符串排名作为两个部分,然后基数排序,就能得到长度为 (2^k) 的字符串的排名。如图举例子:

其中 (x,y) 就分别是前半部分和后半部分。这两个部分正好在基数排序中看作是两位。然后假设后半部分已经有序,而且前半部分排名相同的必然在倍增后排名依然为连续一段,所以我们可以求出前半部分每个排名倍增后所在的区间。接着,倒序枚举后半部分,并依次在所在区间末尾插入即可。

也可以从上图得知倍增是 (i) 的前半部分其实就是倍增前 (i) 的排名,其后半部分就是就是将要合并的第二部分的起始位置。

还有一点:

如图红圈,有的位置在合并过程中已经没有了后半部分,我们把它当作是该前半部分最小的,这里有点难理解,由于两个部分在基数排序中看作是两位数,换句话说这里的意思就是个位当作是 (0),就成了该十位中最小的数。

代码:

int n, m;
int sa[N], c[N], x[N], y[N];
char s[N];

void Solve()
{
	for (int i = 1; i <= m; i++) c[i] = 0;
	for (int i = 1; i <= n; i++) c[x[i] = s[i]]++;
	for (int i = 2; i <= m; i++) c[i] += c[i - 1];
	for (int i = n; i; i--) sa[c[x[i]]--] = i;
	
	for (int k = 1; k <= n; k <<= 1)
	{
		int num = 0;
		for (int i = n - k + 1; i <= n; i++) y[++num] = i; //The numer has no the latter part
		for (int i = 1; i <= n; i++) 
			if(sa[i] > k) y[++num] = sa[i] - k;
		
		for (int i = 1; i <= m; i++) c[i] = 0;
		for (int i = 1; i <= n; i++) c[x[i]]++;
		for (int i = 2; i <= m; i++) c[i] += c[i - 1];
		for (int i = n; i; i--) sa[c[x[y[i]]]--] = y[i], y[i] = 0;
		
		swap(x, y);
		x[sa[1]] = 1, num = 1;
		for (int i = 2; i <= n; i++)
			x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k])? num: ++num;
		m = num;
		if (n == m) break;
	}
	return ;
}

int main()
{
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	scanf ("%s", s + 1);
	n = strlen(s + 1), m = 122;
	Solve();
	for (int i = 1; i <= n; i++)
		printf ("%d ", sa[i]);
    return 0;
}

原文地址:https://www.cnblogs.com/GJY-JURUO/p/14403298.html