字符串基礎算法總結


Trie

原理

不講了吧……就是一個點對應一個字符,很基本的思路。如果不會看 這里

模板
//Author: RingweEH
//UVA1401 Remember the Word
void Insert( char *s )
{
	int l=strlen(s),p=0;
	for ( int i=0; i<l; i++ )
	{
		int ch=s[i]-'a';
		if ( !tr[p][ch] ) tr[p][ch]=++tot,val[tot]=0;
		p=tr[p][ch];
	}
	val[p]=1;
}

int Query( char *s,int st )
{
	int res=0,p=0;
	for ( int i=st; i<len; i++ )
	{
		int ch=s[i]-'a'; p=tr[p][ch];
		if ( !p ) return res;
		if ( val[p] ) (res+=dp[i+1])%=Mod;
	}
	return res;
}

int main()
{
	int cas=1;
	while ( scanf("%s",str )!=EOF )
	{
		tot=0; memset( tr,0,sizeof(tr) ); memset( val,0,sizeof(val) );

		scanf( "%d",&n );
		for ( int i=0; i<n; i++ ) scanf( "%s",ch ),Insert(ch);

		len=strlen(str); dp[len]=1;
		for ( int i=1; i<=len; i++ ) dp[len-i]=Query(str,len-i);

		printf("Case %d: %d\n",cas++,dp[0] );
	}

	return 0;
}

練習 - UVA1462 Fuzzy Google Suggest

首先對給出的字符串集建 Trie 。對於每一次搜索操作,在 Trie 上進行兩次 DFS(清理也要,數據范圍三百萬不可能 memset ,代碼中將計算貢獻和清除一起寫了)

第一次 DFS 對於搜索串進行處理,如果匹配那么直接搜索,否則減少一次剩余修改次數並繼續搜索。這個過程中,為了計算貢獻需要打 tag ,如果是路徑上經過的 Trie 節點就標記為 \(1\) ,如果是結尾就標記為 \(2\) .

第二次 DFS 對 tag 數組清除,並累加第一次出現 \(2\) 的位置的貢獻。

Code
//Author: RingweEH
void Insert( char *s )
{
	int p=0,l=strlen(s);
	for ( int i=0; i<l; i++ )
	{
		int ch=s[i]-'a';
		if ( !tr[p][ch] ) 
		{ 
			tot++; memset(tr[tot],0,sizeof(tr[tot])); 
			val[tot]=0; tr[p][ch]=tot;
		}
		p=tr[p][ch]; val[p]++;
	}
}
void DFS( int p,int dep,int x )
{
	if ( x<0 ) return;
	if ( vis[p]==0 ) vis[p]=1;
	if ( dep==len ) { vis[p]=2; return; }
	int ch=s[dep]-'a';
	if ( tr[p][ch] ) DFS(tr[p][ch],dep+1,x);
	DFS(p,dep+1,x-1);
	for ( int i=0; i<26; i++ )
		if ( tr[p][i] ) DFS(tr[p][i],dep,x-1),DFS(tr[p][i],dep+1,x-1);
}
void Clear( int p,bool fl )
{
	if ( vis[p]==0 ) return;
	if ( fl && vis[p]==2 ) ans+=val[p],fl=0;
	for ( int i=0; i<26; i++ )
		if ( tr[p][i] ) Clear(tr[p][i],fl);
	vis[p]=0;
}

KMP

原理

對於一個模板串 abbaaba ,如果匹配到最后一個字符失配,那么應該從模板串的第三個開始重新匹配。過程主要通過 \(nxt\) 數組實現,\(nxt[i]\) 表示以 \(s[i]\) 結尾的后綴和 \(s\) 的前綴所能匹配的最大長度( \(s\) 是模板串)(如果下標從 \(1\) 開始那么最大長度也就是最靠后的位置,通俗點說就是到 \(i\) 失配了,模板串指針就跳到這個位置繼續匹配)

KMP詳解 FFT版

下圖中 \(j\) 指針指向的是模板串的匹配位置。

模板
//Author: RingweEH
//P3375 【模板】KMP字符串匹配
int main()
{
	scanf( "%s%s",sa+1,sb+1 );

	lena=strlen(sa+1); lenb=strlen(sb+1);
	for ( int i=2,j=0; i<=lenb; i++ )
	{
		while ( j && sb[i]!=sb[j+1] ) j=nxt[j];
		if ( sb[j+1]==sb[i] ) j++;
		nxt[i]=j;
	}
	for ( int i=1,j=0; i<=lena; i++ )
	{
		while ( j && sb[j+1]!=sa[i] ) j=nxt[j];
		if ( sb[j+1]==sa[i] ) j++;
		if ( j==lenb ) { printf("%d\n",i-lenb+1 ); j=nxt[j]; }
	}

	for ( int i=1; i<=lenb; i++ ) printf("%d ",nxt[i] );

	return 0;
}

擴展KMP

定義母串 \(S\) 和子串 \(T\) ,設 \(S\) 的長度為 \(n\)\(T\) 的長度為 \(m\) ,求 \(T\)\(S\) 的每一個后綴的最長公共前綴。

\(extend[i]\) 表示 \(T\)\(S[i,n-1]\) 的最長公共前綴,要求出所有 \(extend[i](0\leq i<n)\)

思想

從左到右依次計算 \(extend\) ,在某一時刻,設 \(extend[0\cdots k]\) 已經計算完畢,並且之前匹配過程中所達到的最遠位置為 \(P\) 。所謂最遠位置,就是 \(i+extend[i]-1\) 的最大值 \((0\leq i\leq k)\) ,並且設取這個最大值的位置為 \(pos\)

現在要計算 \(extend[k+1]\) ,根據 \(extend\) 的定義,可以推斷出 \(S[pos,P]=T[0,P-pos]\) ,從而 \(S[k+1,P]=T[k-pos+1,P-pos]\) ,令 \(len=nxt[k-pos+1]\) ,分情況討論:

  • \(k+len<P\)

exKMP1

上圖中,\(S[k+1,k+len]=T[0,len-1]\) ,然后 \(S[k+len+1]\) 一定不等於 \(T[len]\) ,因為如果它們相等,則有 \(S[k+1,k+len+1]=T[k+pos+1,k+pos+len+1]=T[0,len]\) ,和 \(nxt\) 數組的定義不符,所以 \(extend[k+1]=len\) .

  • \(k+len>=P\)

img

上圖中,\(S[p+1]\) 之后的字符都還未進行過匹配,所以就要從 \(S[P+1]\)\(T[P-k+1]\) 開始一一匹配,直到發生失配為止,當匹配完成后,如果得到的 \(extend[k+1]+(k+1)>P\) 則要更新 \(P\)\(pos\)

練習 - UVA1358 Generator

首先對模板串 KMP 預處理。設 \(dp[i]\) 表示末尾匹配了模板串長度為 \(i\) 的前綴所需要的次數期望。

每次枚舉可能出現的字符,設當前生成了 \(j\) ,且 \(j\) 不是 \(s[i+1]\) ,跳 \(nxt\) 跳到了 \(k\) ,那么匹配 \(k\) 個到匹配 \(i\) 個還需要 \(dp[i]-dp[k]\) 次。

\(f(i)=1+\sum_{i=1}^n(dp[i-1]-dp[fail(j)])/n+\dfrac{n-1}{n}f(i),dp[i]=dp[i-1]+f(i)\)

Code
//Author: RingweEH
void Get_Nxt( char *s ) {}
int main()
{
    int T; scanf( "%d",&T );
    for ( int cas=1; cas<=T; cas++ )
    {
        if ( cas>1 ) puts("");
        scanf( "%d%s",&n,str+1 );

        Get_Nxt(str); dp[0]=0; 
        for ( int i=1; i<=len; i++ )
        {
            dp[i]=dp[i-1]+n;
            for ( int j=0; j<n; j++ )
            {
                if ( str[i]=='A'+j ) continue;
                int p=i-1;
                while ( p && str[p+1]!=(j+'A') ) p=nxt[p];
                if ( str[p+1]==j+'A' ) p++;
                dp[i]+=dp[i-1]-dp[p];
            }
        }

        printf("Case %d:\n",cas );
        printf("%lld\n",dp[len] );
    }

    return 0;
}

關於 kmp 算法中 next 數組的周期性質

來源

約定: \(nxt[?]\) 不同於 \(nxt[i]\) ,定義為 \(nxt[i]\) 的候選項之一。

結論

對於某一字符串 \(S[1\to i]\) ,在眾多的 \(nxt[i]\) 候選項中,如果存在一個 \(nxt[i]\) 使得 \(i\bmod (i-nxt[i])==0\) ,那么 \(S[1\to (i-nxt[i])]\) 可以成為 \(S[1\to i]\) 的循環節,循環次數為 \(\dfrac{i}{i-nxt[i]}\)

推論1

\(i-nxt[i]\) 可以整除 \(i\) ,那么 \(s[1\to i]\) 具有長度為 \(i-nxt[i]\) 的循環節,即 \(s[1\to i-nxt[i]]\)

推論2

\(i-nxt[?]\) 整除 \(i\) ,那么 \(s[1\to i ]\) 具有長度為 \(i-nxt[?]\) 的循環節,即 \(s[1\to i-nxt[?]]\)

推論3

任意一個循環節長度必定是最小循環節長度倍數。

推論4

如果 \(i-nxt[i]\) 不能整除 \(i\) ,\(s[1\to i-nxt[?]]\) 一定不能作為循環節。

擴展
  1. 如果 \(i-nxt[i]\) 不能整除 \(i\) ,一定不存在循環節,\(i-nxt[?]\) 一定都不可整除
  2. 如果 \(s[1\to m]\)\(s[1\to i]\) 的循環節,\(nxt[i]\) 一定為 \(i-m\) .
  3. \(m-i-nxt[i],j=nxt[?]=>nxt[j]=j-m\)

AC自動機

解決問題

多個模板串匹配一個文本串。

原理

大致思想是,KMP是線性的字符串加上失配邊,AC自動機就是Trie加上失配邊。 圖解AC自動機

模板
//Author: RingweEH
void Insert( char *s )
{
    int p=0,l=strlen(s);
    for ( int i=0; i<l; i++ )
    {
        int ch=s[i]-'a';
        if ( !tr[p][ch] ) tr[p][ch]=++tot;
        p=tr[p][ch];
    }
    cnt[p]++;
}

void GetFail()
{
    queue<int> q; fail[0]=0;
    for ( int i=0; i<26; i++ ) 
        if ( tr[0][i] ) fail[tr[0][i]]=0,q.push(tr[0][i]);
    while ( !q.empty() )
    {
        int nw=q.front(); q.pop();
        for ( int i=0; i<26; i++ )
            if ( tr[nw][i] )
                fail[tr[nw][i]]=tr[fail[nw]][i],q.push(tr[nw][i]);
            else tr[nw][i]=tr[fail[nw]][i];
    }
}

int Query( char *s )
{
    int p=0,res=0,l=strlen(s);
    for ( int i=0; i<l; i++ )
    {
        p=tr[p][s[i]-'a'];
        for ( int j=p; j && cnt[j]!=-1; j=fail[j] ) res+=cnt[j],cnt[j]=-1;
    }
    return res;
}

練習 - UVA1399 Puzzle

AC自動機+DP。首先對於給出的禁止串建自動機,在每個末尾打標記,在得到 \(fail\) 指針的同時注意要傳遞標記。設 \(dp[u]\) 表示 Trie 樹上從節點 \(u\) 往下,不經過標記的最大長度,就是子節點 \(dp\) 值取 \(\max+1\) 。然后一遍 DFS 找是否能出現循環。

Code
//Author: RingweEH
namespace ACMachine
{
    int tr[N][26],cnt[N],tot,fail[N];
    void Init_ACM() { tot=0; memset(tr[0],0,sizeof(tr[0])); }
    void Insert( char *s ) {}
    void GetFail(){}
}
using namespace ACMachine;

bool Find( int u )
{
    fl[u]=1;
    for ( int i=0,v; i<n; i++ )
    {
        v=tr[u][i];
        if ( vis[v] ) return 1;
        if ( !fl[v] && !cnt[v] )
        {
            vis[v]=1; if ( Find(v) ) return 1; vis[v]=0;
        }
    }
    return 0;
}

int DFS( int u )
{
    if ( vis[u] ) return dp[u];
    vis[u]=1; dp[u]=0;
    for ( int i=n-1; i>=0; i-- )
        if ( !cnt[tr[u][i]] )
        {
            int tmp=DFS(tr[u][i])+1;
            if ( dp[u]<tmp ) dp[u]=tmp,ans[u][0]=tr[u][i],ans[u][1]=i;
        }
    return dp[u];
}

void Write( int u )
{
    if ( ans[u][0]==-1 ) return;
    printf("%c",ans[u][1]+'A' ); Write(ans[u][0]);
}

int main()
{
//freopen( "exam.in","r",stdin );

    int T; scanf("%d",&T );
    while ( T-- )
    {
        Init_ACM(); scanf("%d%d",&n,&m);
        while ( m-- ) scanf("%s",str),Insert(str);

        GetFail();
        memset(fl,0,sizeof(fl)); memset(vis,0,sizeof(vis)); vis[0]=1;
        if ( Find(0) ) { puts("No"); continue; }
        memset( vis,0,sizeof(vis) ); memset( ans,-1,sizeof(ans) );
        if ( DFS(0)==0 ) puts("No");
        else Write(0),puts("");
    }

    return 0;
}
  • AC自動機+線段樹優化DP :UVA1502 GRE Words

Manacher

解決問題

最長回文子串:給定一個字符串,求它的最長回文子串長度。

原理

一個很簡單的想法是枚舉對稱中心,向兩邊擴展。這樣做的缺陷在於回文子串長度的奇偶性導致對稱軸不確定。所以可以在原串首尾和兩兩字符之間插入一個無關字符,這樣串長都是奇數且原有回文性質不變。

定義:回文半徑:一個回文串中最左(右)位置的字符到其對稱軸的距離 ,用 \(p[i]\) 表示第 \(i\) 個字符的回文半徑。例如:

    char : # a # b # c # b # a #
    p[i] :   1 2 1 2 1 6 1 2 1 2 1
p[i] - 1 : 0 1 0 1 0 5 0 1 0 1 0
       i :      1 2 3 4 5 6 7 8 9 10 11

顯然,最大的 \(p[i]−1\) 就是答案。

插入完字符之后對於一個回文串的長度為 原串長度 \(\times 2+1\) ,顯然相等。

這樣問題就轉換成了怎樣快速的求出 \(p\) 。用 \(mx\) 表示當前所有字符產生的最大回文子串的最大右邊界, \(id\) 表示產生這個邊界的對稱軸位置。

manacher01

設已經求出了\(p[1...7]\) ,當 \(i<mx\) ,因為 \(id\) 被更新過了,而 \(i\)\(id\) 之后的位置,第 \(i\) 個字符一定落在 \(id\) 右邊。

記串 \(i\) 表示以 \(i\) 為對稱軸的回文串,\(j\)\(id\) 同理。

情況1:i < mx

利用回文串的性質,對於 \(i\) ,可以找到一個關於 \(id\) 對稱的位置 \(j=id\times 2−i\) ,進行加速查找

(1) :串 \(j\) 在串 \(id\) 內部,顯然 \(p[i]=p[j]\) 且串 \(i\) 不能再擴張(否則 \(p[j]\) 也可以)

manacher02

(2) :串 \(j\) 左端點與串 \(id\) 左端點重合,此時 \(p[i]=p[j]\) 但串 \(i\) 可以再擴張。

manacher03

(3) :串 \(j\) 左端點在串 \(id\) 左端點左側。此時 \(p[i]=mx-i\) ,只能確定串 \(i\)\(mx\) 以內部分回文,串 \(i,j\) 不一定相同。

manacher04

這時串 \(i\) 是不可以再向兩端擴張的。如果可以,如下圖,(這里 \(p[j]>p[i]\) ,那么 \(p[j]\ge p[i]+1\) ,截取 \(a,b\) 使得和擴張 \(1\) 之后的 \(i\) 相同顯然不會影響正確性),有 \(d=c\) ,所以 \(c=b\) ,又因為 \(a=b\) 所以 \(a=d\) ,這樣串 \(id\) 就能擴張,矛盾。

manacher05

情況2:i >= mx

顯然 \(p[i]=1\) .

5c7a2e7240b38.jpg

注意,這里的 \(p[i]\) 的情況討論均指“可以從 \(id\)\(mx\) 中繼承”的部分而不是最終的結果,也就是已經處理過的不會再進行處理,要處理一定是往后拓展。

模板
//P3805 【模板】manacher算法
//Author: RingweEH
const int N=2.2e7+10;
int len,pos[N];
char s[N],str[N];

void Init()
{
    len=strlen(s); str[0]='@'; str[1]='#'; int j=2;
    for ( int i=0; i<len; i++ ) str[j++]=s[i],str[j++]='#';
    str[j]='\0'; len=j;
}

int main()
{
    scanf("%s",s); Init();

    int ans=-1,mx=0,id=0;
    for ( int i=1; i<len; i++ )
    {
        if ( i<mx ) pos[i]=min(pos[id*2-i],mx-i);
        else pos[i]=1;
        while ( str[i+pos[i]]==str[i-pos[i]] ) pos[i]++;
        if ( pos[i]+i>mx ) mx=pos[i]+i,id=i;
        ans=max(ans,pos[i]-1);
    }

    printf("%d\n",ans );

    return 0;
}

練習 - UVA1470 Casting Spells

其實就是找連續出現兩次的偶長度回文串。

暴力做法是枚舉並判斷右邊是否也是相同的回文串,考慮 Manacher 的性質。

設現在在位置 \(i\) ,由於 Manacher 把偶數串轉化為了奇數,所以要求 \(i\) 是分隔符 # 。當回文半徑為 \(4\) 的倍數時,左半邊的串長就是偶數,中心為 \(i-r/2\) (其中 \(r\) 是回文半徑)。只要左半邊中心的回文半徑不小於 \(i-r/2\) 那么就是回文串。

Code
//Author: RingweEH
const int N=3e5+10;
int n,pos[N<<1],len;
char s[N],str[N<<1];

void Init()
{
	int j=0; len=strlen(s);
	for ( int i=0; i<len; i++ ) str[j++]='#',str[j++]=s[i];
	str[j++]='#'; len=j;
}

int main()
{
//freopen( "exam.in","r",stdin );

	int T; scanf("%d",&T);
	while ( T-- )
	{
		scanf("%s",s); Init();

		int mx=0,id=0,ans=0;
		for ( int i=0; i<len; i++ )
		{
			if ( i<mx ) pos[i]=min(pos[2*id-i],mx-i);
			else pos[i]=1;
			while ( i+pos[i]<len && i-pos[i]>=0 && str[i+pos[i]]==str[i-pos[i]] )
			{
				int x=pos[i];
				if ( str[i]=='#' && x%4==0 && pos[i-x/2]>=x/2 ) ans=max(ans,x);
				pos[i]++;
			}
			if ( i+pos[i]>mx ) mx=i+pos[i],id=i;
		}

        printf( "%d\n",ans );
	}

	return 0;
}

最小表示法

定義

字典序最小的循環同構串。(就是整體左移或右移,環狀)

原理

考慮兩個字符串 $A,B $ ,在 \(S\) 中的起始位置為 \(i,j\) ,且前 \(k\) 位相等。 如果 \(A[i+k]>B[j+k]\) ,那么對於任何一個字符串 \(T\) ,開頭位於 \(i\to i+k\) 之間,一定不會成為最優解。所以 \(i\) 可以直接跳到 \(i+k+1\)

復雜度 \(O(n)\).

模板
//UVA719 Glass Beads 
//【模板】最小表示法
//Author: RingweEH
const int N=3e4+10;
int n,len;
char str[N];

int main()
{
//freopen( "exam.in","r",stdin );

	int T; scanf("%d",&T);
	while ( T-- )
	{
		scanf("%s",str); len=strlen(str);
		int k=0,i=0,j=1;
		while ( k<len && i<len && j<len )
		{
			int tmp=str[(i+k)%len]-str[(j+k)%len];
			if ( tmp==0 ) k++;
			else
			{
				if ( tmp<0 ) j+=k+1;
				if ( tmp>0 ) i+=k+1;
				if ( i==j ) i++;
				k=0;
			}
		}
		printf("%d\n",min(i,j)+1 );
	}

	return 0;
}

參考

  • OI-Wiki
  • exKMP
  • 找不到 Manacher 配圖的作者博客了 /shake


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM