今天我們用了一個上午學習了字符串中的kmp,最長迴文子串和擴展kmp算法,下午在編相應的裸題。
KMP算法
我們有一個長度爲n的字符串S,長度爲m的字符串T,問T在S裏出現了幾次?
這題是我們平時最經常遇到的字符串問題。這題暴力搜索的話要o(nm)。(當然用hash只需要o(n))我們暴力搜索時會枚舉S每一位爲開頭,然後比較S[i…i+m-1[與T[1…m]爲匹配,之後又繼續往後推一位後再次匹配。(如圖)
我們發現,其實我們在第一次往後移時,S[2]對應下來的TT[1]時重複匹配了的。因爲我們一開始時(藍色)匹配失敗後已經得知S[1…2]與T[1…2]時對應匹配的,而T[2]又和T[1]互相匹配,那麼就可以知道T[1]=T[2]=S[2],所以第一次後移(紅色)時,T[1]死不需要匹配的,那麼我們怎麼必變這種重複比較匹配的情況呢?
我們發現,我們從一開始和第一次往後移的兩個字符串的室友重複部分的T1[2]=T2[1],所以我們每次在T與S匹配失敗後,就直接找到從開頭到成功匹配部分的最後一位這一段與下一次往移的字符串的連續成功匹配部分,然後直接跳到下次可能匹配到的地方。
那麼我們怎麼找到下次應該跳到的地方呢?我們可以用一個next數組存下來。計算next數組時,我們可以像匹配S與T那樣,直接跳到之前已經計算過得next數組,然後從之前匹配到的地方往下繼續匹配。
KMP代碼:(下表從0開始):
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
int T;
char s[1000007],t[10007];
int next[10007];
int m,n;
void _read()
{
scanf("%s",t);
m=strlen(t);
scanf("%s",s);
n=strlen(s);
}
void makenext() //計算next
{
next[0]=-1;
int k=-1;
for(int i=1;i<m;i++)
{
while(k>=0 && t[i]!=t[k+1]) k=next[k];
if(t[i]==t[k+1]) k++;
next[i]=k;
}
}
int _get() //T與S匹配
{
int k=-1;
int ans=0;
for(int i=0;i<n;i++)
{
while(k>=0 && s[i]!=t[k+1]) k=next[k];
if(s[i]==t[k+1]) k++;
if(k==m-1) ans++,k=next[k];
}
return ans;
}
int main()
{
scanf("%d",&T);
for(;T>0;T--)
{
_read();
memset(next,0,sizeof(next));
makenext();
cout<<_get()<<endl;
}
return 0;
}
manacher(最長迴文子串)
給定一個長度爲n字符串S,現在要從中找出一個迴文的子串T。字符串A是迴文的,當且僅當A反轉後的A’和A完全相等。問T可能的最大長度?
迴文串也是我們在字符串中經常遇到的問題。
暴力做法1:枚舉所有子串的開頭和結尾,然後判斷迴文——o(n^3)
暴力做法2:枚舉迴文串的中間點,然後向兩邊延伸,同時判斷是否迴文——o(n^2)
我們從暴力做法2中,其實就能看出迴文串的性質:迴文串對稱中心的兩邊是完全對稱的。這就意味着一個迴文串被包含在另一個大回文串中,那麼它的相對於大的迴文串的對稱串也會是一個迴文串。這樣我們就可以儘量減少時間複雜度。(如圖)
我們發現,找到對稱後有三種情況。
- 像在s1中包含着對稱的兩個迴文串s2,而且s2的左右兩邊的字母都還是在迴文串的範圍內那麼s2所包含的範圍就是它對稱串那麼多。
- 像在s3中的s4,因爲s4有一邊在迴文串的邊界上,所以s4要嘗試向邊界突破,變成s5。
- 像在s5中的s2,因爲s2有一部分是在s5的外面,所以s2關於s5的對稱串的範圍要縮短到s5的邊界上。
所以我們分析出所有情況後,可以推出算法。
首先從左到右枚舉對稱中心,然後看一下關於之前的最右端伸得最遠的迴文串的對稱串似乎否對稱。①如果對稱穿超出最遠迴文串的範圍,那麼現在掃到的迴文串就是能是先定爲到最遠迴文串的範圍,然後向最右端突破;②如果對稱串沒有達到範圍,那麼當前迴文串範圍就是對稱穿的範圍;③如果對稱串的範圍剛好達到了最遠迴文串範圍,那麼當前迴文串就先定爲到最遠迴文串範圍,然後向最右端突破。如果突破了最右端,那麼就把之前最右端的對稱中心改爲當前對稱中心,然後繼續向右掃。
但是因爲我們枚舉的是中間點,所以出現迴文串是偶數的情況。那麼我們就可以在這一大串字符串中每隔一個字符就插入一個原串沒有出現過的字符,這樣就可以避免出現偶數串的情況。
manacher代碼:
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
char c[110007];
char s[2*110007];
int ans[2*110007];
int slen;
int main()
{
while(EOF!=scanf("%s",c))
{
memset(ans,0,sizeof(ans));
int clen=strlen(c);
if(clen==1)
{
cout<<1<<endl;
continue;
}
slen=0;
for(int i=0;i<clen;i++) //插入避免偶數
{
slen++;s[slen]='#';
slen++;s[slen]=c[i];
}
s[++slen]='#';s[++slen]='$';
s[0]='@';
ans[0]=ans[1]=ans[2]=0;
int p=2;
for(int i=3;i<slen;i++) //p爲最遠串的對稱中心
//ans表示迴文串半徑
{
ans[i]=max(0,min(ans[p*2-i],p+ans[p]-i)); //判斷情況
while(s[i-ans[i]]==s[i+ans[i]]) ans[i]++; //突破
if(ans[i]+i>ans[p]+p) p=i; //更新
}
int maxp=2;
for(int i=2;i<slen-1;i++) //求最長的串
if(ans[maxp]<ans[i]) maxp=i;
cout<<ans[maxp]-1<<endl;
}
return 0;
}