zoukankan      html  css  js  c++  java
  • 字符串算法总结

    本文主要介绍 Manacher算法KMP算法扩展KMP算法AC自动机后缀数组 这几个简单的字符串算法。
    以下内容均属于个人理解,如有错误请指出,不胜感激。

    Manacher算法

    Manacher算法,是用来解决有关回文串的问题的算法。

    问题:

    给定一个字符串(S),长度为(n),求它的最长回文子串的长度?
    (1leq nleq 1.1 imes 10^7)

    先不讲正解是什么,先想想暴力怎么做。
    不难想到我们可以直接枚举中间点然后左右扩展,判一下奇偶就可以了。复杂度(O(n^2))
    我们可以考虑一下优化。
    在原字符串的首尾和每个字符中间增加 原字符串中没有的字符 ,比如说(\%),这样就可以不必枚举奇偶了。
    举个栗子,字符串(abc)在加上(\%)之后会变成(\%a\%b\%c\%)(abcd)加上之后会变成(\%a\%b\%c\%d\%),字符串长度就都是奇数了。
    以字符串(12212321)为例,经过上一步,变成了(S=\%1\%2\%2\%1\%2\%3\%2\%1\%)
    我们设置辅助数组(p[i])来记录以字符(S[i])为中心的最长回文子串向左/右扩张的长度(包括(S[i])),下面的图片中用#代替 (\%)

    通过观察我们不难看出(p[i]-1)就是以(S[i])为中心的回文子串的长度。其实这东西证明也是很简单的。
    假设我们(p[i]=3),然后假设从(s[i])往右的(3)个字符为(\%a\%),因为(p)数组保留的是半径,所以我们还原该回文串之后就是(\%a\%a\%)
    因为(\%)都是我们补充的,所以在半径中除去(\%)再乘(2)就是实际回文串的长度。而求实际在原字符串中的字符个数又有两种情况。

    1. 回文串中心是(\%)上面我们已经讨论过了,这里就不重复说了。
    2. 回文串中心是原字符串中的字符。举个栗子(a\%a\%(p[i]=4)),显然这里的有用的字符就是(aa),因为这是半径,所以我们还要乘(2),但是由于该回文串的中心是原字符串中的字符,所以中间那个字符会算两次,所以我们还要减去中间的那个字符,即(2 imes2-1),也就是(4-1),也就是(p[i]-1)了。

    但是我们还要考虑一点,我们上面讨论的都是结尾字符以(\%)的情况,那会不会结尾字符有原字符串中的字符呢?
    答案显然是 不可能 的,因为如果有这种情况的话,我们可以再往两边扩展一个(\%)(每两个字符中间都有一个(\%)),就变成了上面我们讨论的两种情况了。
    (cdot) 再设置两个辅助变量
    (id)为当前我们已知的右边界最大的回文子串的中心,(mx)(id+p[id])也就是最大的右边界。如下图。

    假设我们当前要求的位置为(i(i<mx)),我们可以找到(i)关于(id)的对称点(j),因为(id)为中点,所以(id)左侧和右侧字符是 完全一样 的。
    所以我们可以利用(j)来加速(p[i])的求解。因为(p[i])是可以等于(p[j])的。
    但是我们注意到,(mx)右边的部分是有可能不等于(id-p[id])(即(mx)的对称点)的左边部分的。所以(p[i])应该小于等于(mx-i)。至于(mx)右侧的部分,我们就只能暴力扩展了。
    如果(i geq mx)的话,那我们就只能先暂令(p[i]=1)然后同样暴力扩展了。扩展完之后更新(mx)(id)
    模板题Manacher算法 下面放代码,代码中用#代替(\%)

    #include<bits/stdc++.h>
    using namespace std;
    char s[32000005],s_new[32000005];
    int p[32000005];
    int get() {
    	s_new[0]='$';
    	s_new[1]='#';
    	int k=2;
    	int n=strlen(s);
    	for(int i=0; i<n; i++) {
    		s_new[k++]=s[i];
    		s_new[k++]='#';
    	}
    	s_new[k]='';
    	return k;
    }
    void manacher() {
    	int len=get();
    	int mx=0,id=0,ans=-1;
    	for(int i=1; i<=len; i++) {
    		if(i<mx)
    			p[i]=min(mx-i,p[2*id-i]);
    		else
    			p[i]=1;
    		while(s_new[i-p[i]]==s_new[i+p[i]])
    			p[i]++;
    		if(i+p[i]>mx) {
    			mx=i+p[i];
    			id=i;
    		}
    		ans=max(ans,p[i]-1);
    	}
    	printf("%d
    ",ans);
    }
    int main() {
    	scanf("%s",s);
    	manacher();
    	return 0;
    }
    

    KMP算法

    问题引入

    给你一个文本串(S),长度为(n),再给你一个模式串(T),长度为(m),求模式串在文本串中出现的位置?
    (1 leq n,mleq 10^6)

    这是一个很很很经典的问题了。
    因为篇幅问题(其实是作者懒得打了),这里就直接进入主题了。
    依然是举例子。看一下下面这组样例。

    我们用指针(i)(j)来表示(S[i-j+1,i]=T[1,j])
    也就是说(i)是随着(j)的增加而增加的,而当(j=m)时,就意味着我们在文本串(S)中找到了一个模式串(T)。我们令上图中(i=j=5)
    接下来我们就要比较(S[i+1])(T[j+1])了,很明显,模式串和文本串将在这一位失配,也就是下图中的红色位置。

    这个时候我们该怎么办呢?想一想。我们研究这个算法的目的是什么?是不是就是提升速度?那么速度的瓶颈是什么?是不是就是模式串和文本串匹配的次数?
    也就是说,如果我们模式串和文本串的匹配次数越少,这个算法的时间复杂度就越少(显而易见)
    所以当我们失配的时候,我们是不是应该将(j)指针往前移,移到某一位置使模式串(T[1,j])再次和文本串(S[i-j+1,i])相匹配,并且,(j) 越大越好 (显而易见),这样就可以使匹配次数尽可能地减少了。具体点,就是说我们要在模式串(T[1,j])中再找到一个位置(j'),使得(T[1,j']=T[j-j'+1,j]),因为这样我们才可以保证此时的(j')与之前的(j)的性质是完全一样的(即还能与文本串匹配成功),然后再比较(T[j'+1])(S[i+1])是否相等。在上面这个例子中,(T[3,5]=T[1,3]),所以当(T[j+1])(S[i+1])失配时,我们可以让(j)重新等于(3),这样文本串和模式串还是匹配的。(因为之前的匹配让我们知道(S[i-j+1,i]=T[1,j]),所以(T[j-j'+1,j]=S[i-j'+1,i]),就有(T[1,j']=T[j-j'+1,j]=S[i-j'+1,i]))。如下图。

    注意此时,(i)指针的位置是 不动 的。然后我们继续匹配(T[j'+1])(S[i+1]),我们发现他们两个是相等的,所以(i,j)同时加(1)。然后继续上述过程,直到(j=m)
    通过我们前面模拟的那一步将(j)调小至(j'),我们发现(j)的减小只与模式串有关,而与(i)无关,因此我们完全可以设置一个辅助数组来实现(j)的快速跳动。
    我们定义(next[i])表示在 模式串 中以(i)结尾的子串的后缀与该子串的前缀 (非本身) 的最长匹配长度。
    说起来可能有点绕,我们仍用上面的例子解释一下这句话。
    (j=5)时,(next[5]=3),为什么呢?因为在(ababa)这个子串中,固然有前缀(a)(即(T[1]))等于后缀(a)(即(T[5])),但它不是最长的。前缀(aba)(即(T[1,3]))等于后缀(aba)(即(T[3,5])),并且没有其他的比这个前缀更长的前缀能与该子串的后缀相匹配(该子串本身除外)。所以(next[5]=3)。为什么要最长?还是上面那句话,这样就可以使匹配次数尽可能地减少了。假设我们已经知道了所有的(next[i]),我们再来模拟一遍上面的样例。
    经过第一次失配后,一直直到(i=7),文本串都是与模式串相匹配的。如下图。

    我们接下来要匹配(S[8])(T[6]),我们发现失配了,所以(j)指针要往前跳,因为(nxet[5]=3),所以(j)指针再一次跳到(3)这个位置。如下图。

    此时(i=7,j=3),我们继续匹配(S[8])(T[4]),发现还是失配,所以(j)还要跳,我们通过观察得知(next[3]=1),所以令(j=1),变成下面这样:

    此时(i)还是等于(7),继续匹配(S[8])(T[2]),可是这个时候它还是不匹配(md怎么这么烦),所以(j)要跳到(next[1]),即(0),所以(j=0)

    终于,我们的(S[8]=T[1])了,所以(i++,j++),可是有时候,就算到(j=0)时它仍然不会和文本串匹配。因此,当(j=0)时,我们增加(i)值但忽略(j),直到出现文本串与模式串匹配。
    至此KMP算法(有限状态自动机的模式匹配算法)主要过程告一段落,于是这一段匹配的代码:

    int j=0;
    for(int i=1; i<=n; i++) {
    	while(j>0&&S[i]!=T[j+1]) j=next[j];//不断跳j,i不变
    	if(S[i]==T[j+1]) j++;//如果相等了,j就加1,继续往后匹配
    	if(j==m)//在文本串中找到一个模式串
    		j=next[j];//继续往后找看文本串中还有没有模式串
    }
    

    求解next数组

    我们再来看一下定义:

    定义(next[i])表示在 模式串 中以(i)结尾的子串的后缀与该子串的前缀 (非本身) 的最长匹配长度。

    由定义可知,(next[1]=0)(因为子串本身不算)。我们可以用(next)(next)
    仍然引用上面的例子,假设我们已经求得(next[1 cdots 4]),现在要求(next[5 cdots 6]),该怎么求呢?

    (next[5])还是很简单的,因为由(next[4]=2)可知,(T[1,2]=T[3,4]),然后又因为(T[3]=T[5]),所以(next[5]=3)

    接下来我们求(next[6])
    因为(T[6])并不等于(T[next[5]+1]),所以我们不能直接像上面那样直接算。
    我们已知(next[3]=1,next[5]=3)也就意味着(T[1]=T[3]=T[5]),所以既然我们在(next[5])处失败,我们可以考虑让(next[6]=next[next[5]]),然后比较(T[6])(T[next[next[5]]]+1)
    如果不相等,我们就让(next[6]=next[next[next[5]]]),然后继续比较(T[6])(T[next[next[next[5]]]]+1),如此往复,直到(0)
    所以求(next)数组代码:

    int j=0;
    next[1]=0;//根据定义next[1]=0
    for(int i=2; i<=m; i++) {
    	while(j>0&&T[j+1]!=T[i]) j=next[j];
    	if(T[j+1]==T[i]) j++;
    	next[i]=j;
    }
    

    时间复杂度

    (O(n+m)),这里就不证明了事实上是作者太菜了

    扩展KMP算法

    前言

    扩展KMP,又名(ExKMP),在外国被称为(Z-Algorithm)

    问题

    存在母串 (S) 和子串 (T) ,设 (|S|=n,|T|=m) ,求 (T)(S) 的每一个后缀的最长公共前缀 ((LCP))
    (1 leq n,mleq 2 imes 10^7)

    我们定义(extend[i])表示在(S)串中以(i)为首字母的后缀与(T)串的前缀的最长匹配长度,求解这个问题的过程实际上就是求(extend)数组。
    我们举一个例子吧。
    (S=aaaaaaaaaabaa),(n=13),
    (T=aaaaaaaaaaa),(m=11)

    通过观察我们不难得到(extend[1]=10)。接下来我们要求(extend[2]),该怎么求呢?我们需不需要从(S[2])开始和(T[1])匹配呢?
    我们先理一理,我们从(extend[1]=10)中可以得到什么?

    [egin{aligned} ecause extend[1]&=10\ herefore S[1,10]&=T[1,10]\ herefore S[2,10]&=T[2,10]......star end{aligned}]

    然后我们知道,我们求(extend[2])就是等于求(S[2,10])(T)的前缀的最长匹配长度。
    所以用上我们推导得到的(S[2,10]=T[2,10]),进一步转化,将上面那句话中的(S[2,10])改为(T[2,10]),
    不难发现求(extend[2])就是等价于求(T[2,10])(T)前缀 的最长匹配长度。

    所以我们可以在这里设置(next)数组,其中(next[i])表示(T[i,m])(T)的前缀的最长匹配长度。注意,这里的(next)数组的定义与(KMP)算法中的(next)数组的定义不一样
    我们看图可知(next[2]=10),所以我们根据(next)的定义可以得出以下推导。

    [egin{aligned} ecause next[2]&=10\ herefore T[2,11]&=T[1,10]\ herefore T[2,10]&=T[1,9] end{aligned}]

    然后联立上面推导得到的式子(star),

    [egin{aligned} ecause T[2,10]&=T[1,9]\ ecause T[2,10]&=S[2,10]\ herefore T[1,9]&=S[2,10] end{aligned}]

    所以至此为止,我们回答了上面的那个问题,答案是不需要的,也就是说,当我们求(extend[2])时,(S[2,10]=T[1,9])我们是已知的,所以,不需要再浪费时间去一一匹配。
    直接从(S[11])(T[10])开始比较,一比较发现不同,所以(extend[2]=9)。上面的都是特殊情况,我们现在再来讨论一般情况。

    求extend数组

    当我们要求(extend[k+1])时,假设我们已知(extend[1cdots k]),我们设(p)(max(i+extend[i]-1)),其中 (i in [1,k]),设取到这个(p)的位置为(a)
    (S)数组的定义可得:

    [S[a,p]=T[1,p-a+1] ]

    [ herefore S[k+1,p]=T[k-a+2,p-a+1] ]

    我们令(L=next[k-a+2])。这里分了两种情况。

    第一种情况:

    (k+L<p)

    由已知条件我们可以得到几个关系:

    [T[k-a+2,k-a+2+L-1]=T[1,1+L-1] ]

    化简得(T[k-a+2,k+L+1-a]=T[1,L]),将上面得到的(S[k+1,p]=T[k-a+2,p-a+1])代入

    [egin{aligned} herefore S[k+1,p]&=T[k-a+2,p-a+1]\ &=T[k-a+2,k-a+2+L-1]+T[k-a+2+L-1+1,p-a+1]\ &=S[k+1,k+1+L-1]+S[k+1+L-1+1,p](ecause k+L<p)\ end{aligned}]

    然后我们再化简一下就是

    [S[k+1,k+L]+S[k+1+L,p]=T[k-a+2,k-a+1+L]+T[k-a+2+L,p-a+1] ]

    所以我们发现红色部分是相等的,蓝色部分肯定不相等,因为如果相等那么就违背了(next)数组的定义了((next[k-a+2]=L+1))
    所以这种情况我们可以直接得到(extend[k+1]=L)。同时(a,p)的值都不变,(k+1 ightarrow k)然后继续求(extend[k+1])

    第二种情况:

    (k+L geq p)

    我们也可以像上面第一种情况一样证出红色部分是相等的,这里就不再赘述,请读者自己思考。
    上图的紫色部分是未知的,因为在计算(extend[1cdots k])的时候,我们最远到达的地方就是(p)(p)以后的部分我们是未曾到达过的,所以们就不知道紫色部分是否相等。
    于是我们就要从(S[p+1] Leftrightarrow T[p-k+1])开始匹配,直到不相等为止,匹配完之后比较(p)(extend[k+1]+(k+1))的大小,如果后者大,就更新(a)(p)

    求next数组

    最后一步就是求(next)数组了。在这之前我们再来看一下(next)数组的定义和(extend)数组的定义。

    我们定义(extend[i])表示在(S)串中以(i)为首字母的后缀与(T)串的前缀的最长匹配长度(Rightarrow)
    我们定义(extend[i])表示(S[i,n])(T)的前缀的最长匹配长度
    我们定义(next[i])表示(T[i,m])(T)的前缀的最长匹配长度

    相信聪明的读者已经发现了什么。其实求解(next)数组就是以(T)为母串,同时以(T)为子串的一个 特殊的扩展KMP
    即用(next)计算(next),读者可以直接使用上文的方法求解。

    关于时间复杂度

    在计算(extend)(next)数组的过程中,只会访问未访问的位置,因此,时间复杂度为(O(n+m))
    模板题扩展KMP(Z函数) 这里放一下求(next)数组和(Z)数组(就是(extend)数组)的代码,输入的字符串下标都是从(1)开始。

    void getNext(){
    	Next[1]=m;
    	int p=1,a=2;
    	while(t[p]==t[p+1]&&p+1<=m) p++;
    	Next[2]=p-1;
    	for(int i=3;i<=m;i++){
    		int k=i-1,L=Next[k-a+2];
    		p=Next[a]+a-1;
    		if(k+L<p)
    			Next[i]=L;
    		else{
    			p=max(p-i+1,0);
    			while(t[p+1]==t[p+i]&&p+i<=m) p++;
    			Next[i]=p;
    			a=i;
    		}
    	}
    }
    void getZ(){
    	int a=1,p=1;
    	while(s[p]==t[p]&&p<=m) p++;
    	Z[1]=--p;
    	for(int i=2;i<=n;i++){
    		int k=i-1,L=Next[k-a+2],p=Z[a]+a-1;
    		if(k+L<p)
    			Z[i]=L;
    		else{
    			p=max(p-i+1,0);
    			while(s[p+i]==t[p+1]&&p+i<=n&&p<=m) p++;
    			Z[i]=p;
    			if(Z[i]+i-1>Z[a]+a-1)
    			a=i;
    		}
    	}
    }
    

    AC自动机

    简单的说一下吧。不想写了。
    其实就是(Trie)(KMP)。不会的可以参考这里

    后缀数组

    现在还不会,以后回来补。但是还是先放上一份我觉得讲得比较好的博客吧,点这里

    参考资料:WC2021李建老师的课件《信息学竞赛中的字符串问题》

  • 相关阅读:
    [Luogu P3626] [APIO2009] 会议中心
    杭电 1869 六度分离 (求每两个节点间的距离)
    杭电 1874 畅通工程续 (求某节点到某节点的最短路径)
    最短路径模板
    杭电 2544 最短路径
    POJ 1287 Networking (最小生成树模板题)
    NYOJ 1875 畅通工程再续 (无节点间距离求最小生成树)
    POJ 2485 Highways (求最小生成树中最大的边)
    杭电 1233 还是畅通工程 (最小生成树)
    杭电 1863 畅通工程 (最小生成树)
  • 原文地址:https://www.cnblogs.com/sbwll/p/14363898.html
Copyright © 2011-2022 走看看