后缀数组解决
多模式串的模式匹配问题
给定一个固定待匹配串 S,长度为 n,然后每次输入一个模式串P,长度为m,要求返回 P 在 S 中的一个匹配或者返回匹配失败。所谓匹配指某个位置 i满足 1≤i≤n-m+1 使得 S[i..(i+m-1)]=P,也即 Suffix(i)的长m的前缀==P。
我们知道,如果只有一个模式串,最好的算法就是 KMP 算法,时间复杂度为 O(n+m),但是如果有多个模式串,我们就要考虑做适当的预处理使得对每个模式串进行匹配所花的时间小一些。最简单的预处理莫过于建立 S 的后缀数组(先在 S 的后面添加'$'),然后每次寻找匹配转化为用二分查找法在 SA 中找到和 P 的公共前缀最长的一个后缀,判断这个最长的公共前缀是否等于 m。
这样,每次比较 P 和一个后缀的复杂度为O(m),因为最坏情况下可能比较了 m 个字符。二分查找需要调用比较的次数为 O(logn),因此总复杂度为O(mlogn),于是每次匹配的复杂度从 O(n+m)变为 O(mlogn),可以说改进了不少。
可是这样仍然不能令我们满足。前面提到 LCP 可以增加后缀数组的威力,
我们来试试用在这个问题上。
我们分析原始的二分查找算法,大体有以下几步:
Step1 令 left=1,right=n,max_match=0。
Step2 令 mid=(left+right)/2(这里“/”表示取整除法)。
Step 3 顺次比较 Suffix(SA[mid])和 P 的对应字符,找到两者的最长公共
前缀 r,并判断出它们的大小关系。若r>max_match 则令 max_match=r,ans=mid。
Step4 若 Suffix(SA[mid])<P 则令 left=mid+1,若 Suffix(SA[mid])>P 则令right=mid-1,若 Suffix(SA[mid])=P 则转至 Step 6。
Step5 若 left<right 则转至 Step 2,否则至 Step 6。
Step6 若 max_match=m 则输出 ans,否则输出“无匹配”。
注意力很快集中在Step 3,如果能够避免每次都从头开始比较Suffix(SA[mid])和 P 的对应字符,也许复杂度就可以进一步降低。
类似于前面求 height 数组,我们考虑利用以前求得的最长公共前缀作为比较的“基础”,避免冗余的字符比较。
在比较 Suffix(SA[mid])和 P 之前,我们先用常数时间计算LCP(mid,ans),然后比较 LCP(mid,ans)和 max_match:
情况一:LCP(mid,ans)<max_match,则说明 Suffix(SA[mid])和 P 的最长公共前缀就是 LCP(mid,ans),即直接可以确定 Step 3 中的 r=LCP(mid,ans),所以可以直接比较两者的第 r+1 个字符(结果一定不会是相等)就可以确定 Suffix(SA[mid])和 P 的大小。这种情况下,字符比较次数为 1 次。
情况二: LCP(mid,ans)≥max_match, 则说明 Suffix(SA[mid])和 Suffix(SA[ans])的前 max_match 个字符一定是相同的, 于是 Suffix(SA[mid])和 P 的前 max_match个字符也是相同的,于是比较两者的对应字符可以从第 max_match+1 个开始,最后求出的 r 一定大于等于原先的 max_match,字符比较的次数为 rmax_match+1,不难看出 Step 3 执行过后 max_match 将等于 r。
设每次 Step 3 执行之后 max_match 值增加的量为∆max。在情况一中,∆max=0,字符比较次数为 1=∆max+1;在情况二中,∆max=r-max_match,字符比较次数为 r-max_match+1,也是∆max+1。综上所述,每次 Step 3 进行字符比较的次数为∆max+1。
总共的字符比较次数为所有的∆max 累加起来再加上 Step 3 执行的次数。所有∆max 累加的结果显然就是最后的 max_match 值,不会超过 len(P)=m,而 Step 3 执行的次数为O(logn),因此总共的字符比较次数为 O(m+logn)。而整个算法的复杂度显然和字符比较次数同阶,为 O(m+logn)。
至此,问题得到圆满解决,通过 O(nlogn)的时间进行预处理(构造后缀数组、名词数组,计算 height 数组,RMQ 预处理),之后就可以在 O(m+logn)的
时间内对一个长度为 m 的模式串 P