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\) 失配了,模板串指針就跳到這個位置繼續匹配)
下圖中 \(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\)
上圖中,\(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\)
上圖中,\(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[?]]\) 一定不能作為循環節。
擴展
- 如果 \(i-nxt[i]\) 不能整除 \(i\) ,一定不存在循環節,\(i-nxt[?]\) 一定都不可整除
- 如果 \(s[1\to m]\) 是 \(s[1\to i]\) 的循環節,\(nxt[i]\) 一定為 \(i-m\) .
- \(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\) 表示產生這個邊界的對稱軸位置。
設已經求出了\(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]\) 也可以)
(2) :串 \(j\) 左端點與串 \(id\) 左端點重合,此時 \(p[i]=p[j]\) 但串 \(i\) 可以再擴張。
(3) :串 \(j\) 左端點在串 \(id\) 左端點左側。此時 \(p[i]=mx-i\) ,只能確定串 \(i\) 在 \(mx\) 以內部分回文,串 \(i,j\) 不一定相同。
這時串 \(i\) 是不可以再向兩端擴張的。如果可以,如下圖,(這里 \(p[j]>p[i]\) ,那么 \(p[j]\ge p[i]+1\) ,截取 \(a,b\) 使得和擴張 \(1\) 之后的 \(i\) 相同顯然不會影響正確性),有 \(d=c\) ,所以 \(c=b\) ,又因為 \(a=b\) 所以 \(a=d\) ,這樣串 \(id\) 就能擴張,矛盾。
情況2:i >= mx
顯然 \(p[i]=1\) .
注意,這里的 \(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