• 字符串进阶算法小结


    前言:

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

    正文:

    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;
    }
    
    
  • 相关阅读:
    给Linux添加google搜索命令
    安卓设置脚本开机自启动
    重读The C programming Lanuage 笔记四:c预处理
    重读The C programming Lanuage 笔记三:简单计算器程序
    重读The C programming Lanuage 笔记二:运算符优先级
    重读The C programming Lanuage 笔记一:类型转换
    【 转】 C/C++结构体和联合体的区别
    python 柯里化 和装饰器
    qq bug 一个
    网站收集
  • 原文地址:https://www.cnblogs.com/GJY-JURUO/p/14403298.html
Copyright © 2020-2023  润新知