字符串小记(KMP, AC自动机, Manacher)

PS:Ubuntu下的WPS总觉得和Windows下编辑的文档的兼容性不好。。

字符串

AC自动机

构造:点A的失败指针指向的是与该结点等价的点B,B表示的字符串是A表示的字符串的suffix(1)(从0开始)。

失败指针指向的状态与本状态拥有最长公共后缀。

那么A的失败指针为A的父亲的失败指针的对应字符儿子,如果没有这个儿子就继续沿失败指针走。

那么代码就很好写了

// 伪代码
void ac_automaton()
{
    static queue<node *> q;
    q.clear(); q.push(root);
    while (!q.empty())
    {
        node *u = q.front(); q.pop();
        for (node *i : u->child)
        {
            if (u == root) i->fail = root;
            else
            {
                Node *p = u->fail;
                while (p && !p->child[i->character]) p = p->fail;
                i->fail = p ? p->child[i->character] : root;
            }
            q.push(i);
        }
    }
}

KMP

KMP用于在目标串中匹配模板串,找到模板串在目标串中的位置。

算法目标是让目标串的指针不回溯(暴力算法有不断回溯目标串指针的弊端)。

任务是始终保持目标串和模板串的指针对应,保证目标串与模板串的指针左侧(不包括指针指向的位置)是完全匹配的

另外跳模板串的指针就相当于移动模板串了(认定指针是对齐的)。

next[j]表示j失配后,在模板串P[1..j-1]中,longest_common_prefix(suf(i),suf(1))最大的那个i。

比如字符串:"ABCDABD"

那么next[6]=2,即P[1..6]中,suf(5)=‘AB'与suf(1)的公共前缀最长='AB',长度为2。

算出next[7]的意义是什么?如果我们一直匹配匹配到位置7了,说明目标串在当前位置,匹配了[1..6],那么为了让目标串的指针不回溯,就要移动模板串的指针,使目标串与模板串的指针再次对应。我们就可以移动到next[7-1=6]=2+1=3位置重启匹配。

此时是:

目标串:ABCDABK

模板串:ABCDABD

第7位不匹配,加粗的表示指针位置
移动指针后:

目标串:ABCDABK

模板串:    ABCDABD

这样就可以让目标串的指针不动,移动模板串了。

而且保证了前面提到的任务。


为什么要这么移动呢?如果让指针不跳这么多,显然指针前是不匹配的,即冗余判断,不可能得到解;或者是匹配模板串的长度不是最优!

但是模板串的指针也不能跳太多,所以跳一次只能从当前跳到与lcp最大的前缀的位置,否则会少考虑一部分必要重新匹配的位置。

所以目标串T的指针只会移动|T|次。


求解next?

需要两个指针,分别表示当前求解next[j],以及j之前匹配到的前缀I的长度

譬如ABCDABD,这里下标从1开始,如果下标从0开始的话i、j和next数组统一减1就可以了。

1. i=0,j=1 next[1]=0

2. i=0,j=2 A!=B next[2]=0

3. i=0,j=3 A!=C next[3]=0

4. i=0,j=4 A!=D next[4]=0

5. i=1,j=5 A=A next[5]=1

6. i=2,j=6 B=B next[6]=2 (如果第7位不匹配了就跳到3继续尝试匹配,因为模板串的5~6和1~2是匹配的,不用再匹配一次了)

7. i=0,j=7 C!=D失配,尝试查找比i更前的已匹配的位置,即i=next[i]=0 next[7]=0

显然,next[j]=max{i}。由于我们约定了next[j]=前缀序号+1,所以从j跳到next[j]就是刚好当前准备尝试匹配的位置。

getFail:

for (i = 0, j = 2; j <= n; ++j) {
	while (i && p[j] != p[i + 1]) i = next[i];
	if (p[i + 1] == p[j]) ++i;
	next[j] = i;
}

find:

for (i = 0, j = 0; j < m; ++j) {
    while (i && t[j] != p[i + 1]) i = next[i];
    if (t[j] == p[i + 1]) ++i;
    if (i == n) // find out.
}



Manacher

void manacher(char *ch, char *a, int *p, int n) { // 从1开始
	int mx = 0, id, i;
	m = 2 * n + 1;
	a[0] = '+'; a[1] = '#'; a[m + 1] = '-';
	for (i = 1; i <= n; ++i) {
		a[i * 2] = ch[i];
		a[i * 2 + 1] = '#';
	}
	for (i = 1; i <= m; ++i) {
		if (mx > i) p[i] = min(mx - i, p[2 * id - i]);
		else p[i] = 1;
		for (; a[i - p[i]] == a[i + p[i]]; ++p[i]);
		if (p[i] + i > mx) mx = p[i] + i, id = i;
	}
}
manacher算法通过在字符间插入'#',将偶长度的回文串转化为奇长度的回文串。
令p[i]表示以i为中心的极大回文串半径。
令mx表示当前已经检查到的回文串向右延伸最远位置,同时记录该回文串的中心id。
那么检查到位置 i 时,如果存在一个回文串覆盖了该位置(即id&mx),那么关于该回文串对称的2*id-i,显然p[i]>=min(mx-i,p[2*id-i])
即如果对称的另外一个中心的回文串的半径,但不能超出检查的回文串id&mx,原因是,如果超出了当前覆盖的回文串,那么就表示,两端点外延伸1个位置的两位置字符不同,那么显然不能被当前的i应用。要从mx-i基础上再暴力检查。
如果向右延伸最远位置没有延伸到i,那么啥性质都没有办法利用,老老实实暴力检查。


后缀自动机

单独开了篇文章....

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值