【總結】字符串知識清單
之前一直跳過了字符串,現在才開始系統地學習,感覺需要記得模板挺多,在這裏列個知識清單總結一下。
(1)字符串Hash
就是把字符串s視為一個B(一般B取不太大的質數)進制的數,用一個數組a來存s的前綴hash值,a用unsigned long long自動溢出比較方便。
一個重要的柿子:hash(s[l~r])=hash(s[r])-hash(s[l-1])*B^(r-l+1)
B的冪要預處理出來。
(2)Manacher
很NB的算法,要花點時間理解。
#include<iostream> #include<cstring> #includeView Code<vector> #include<cstdio> #include<algorithm> using namespace std; int Manacher(string &s){ string t="$#"; for(int i=0;i<s.size();++i){ t+=s[i]; t+="#"; } vector<int> p(t.size(), 0); int Mmid=0,Mlen=0,mx=0,id=0; for(int i=1;i<t.size();++i){ p[i]=mx>i?min(p[id*2-i],mx-i):1; while(t[i+p[i]]==t[i-p[i]]) ++p[i]; if(i+p[i]>mx){ mx=i+p[i]; id=i; } if(p[i]>Mlen){ Mlen=p[i]; Mmid=i; } } return Mlen-1; //return s.substr((Mmid-Mlen)>>1,Mlen-1); } int main(){ int cas=0; string s; while(1){ cin>>s; if(s=="END") break; printf("Case %d: %d\n",++cas,Manacher(s)); } return 0; }
(3)KMP
這個算法我起碼學了3遍,還是經常忘。。。
這份代碼s下標從1開始。
#include<cstdio> #include<cstring> using namespace std; const int N=1005; int m,n,next[N]; char A[N],B[N]; int main(){ while(1){ int ans=0; scanf("%s",A+1); m=strlen(A+1); if(m==1 && A[1]==‘#‘) break; scanf("%s",B+1); n=strlen(B+1); next[1]=0; for(int i=2,j=0;i<=n;++i){ while(j>0 && B[i]^B[j+1]) j=next[j]; if(B[i]==B[j+1]) ++j; next[i]=j; } for(int i=1,j=1;i<=m;++i){ while(j>0 && (B[j+1]^A[i] || j==n)) j=next[j]; if(B[j+1]==A[i]) ++j; if(j==n) ++ans,j=0; } printf("%d\n",ans); } return 0; }View Code
(4)最小表示法
某些題目把形如“abcd”,“bcda”,"cdab","dabc"這些字符串(其實它們叫循環同構串)視為同一字符串,這時我們可以通過求出這些串的最小表示來提高比較和hash的效率。
這份模板代碼s下標從1開始。
可以求出s的最小表示的起始位置。
int minpre(char *s){ int n=strlen(s+1); for(int i=1;i<=n;++i) s[n+i]=s[i]; int i=1,j=2,k; while(i<=n && j<=n){ for(k=0;k<n&&s[i+k]==s[j+k];++k) ; if(k==n) break; //s只由一個字符構成 if(s[i+k]>s[j+k]){ i=i+k+1; if(i==j) ++i; } else{ j=j+k+1; if(i==j) ++j; } } return min(i,j); }View Code
給一道題目:POJ-3349
此題POJ上提交時記得選G++。
#include<cstdio> #include<cctype> #include<cstring> #include<algorithm> using namespace std; const int N=1e5+5,P=99991; inline int read(){ int x=0,w=0;char ch=0; while(!isdigit(ch)) w|=ch==‘-‘,ch=getchar(); while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return w?-x:x; } int n,tot,tmp[12],snow[N][12],tail[N],last[N],minl[N]; int minprs(int *a){ int i=0,j=1,k; while(i<6&&j<6){ for(k=0;k<6&&a[i+k]==a[j+k];++k) ; if(k==6) break; if(a[i+k]>a[j+k]){ i=i+k+1; if(i==j) ++i; } else{ j=j+k+1; if(i==j) ++j; } } return min(i,j); } bool cmp(int pa,int *a,int *b){ int pb,pc,c[12]; for(int i=0;i<12;++i) c[11-i]=b[i]; pb=minprs(b),pc=minprs(c); bool ok=true; for(int i=0;i<6;++i) if(a[pa+i]^b[pb+i]) ok=false; if(ok) return true; ok=true; for(int i=0;i<6;++i) if(a[pa+i]^c[pc+i]) ok=false; if(ok) return true; return false; } int Hash(int *a){ int sum=0; for(int i=0;i<6;++i){ sum=(sum+a[i])%P; //mul=(long long)mul*a[i]%P; } return sum; } bool insert(int *a){ int val=Hash(a); for(int p=tail[val];p;p=last[p]) if(cmp(minl[p],snow[p],a)) return true; ++tot; for(int i=0;i<12;++i) snow[tot][i]=a[i]; minl[tot]=minprs(snow[tot]); last[tot]=tail[val]; tail[val]=tot; return false; } int main(){ n=read(); for(int i=1;i<=n;++i){ for(int j=0;j<6;++j) tmp[j]=tmp[j+6]=read(); if(insert(tmp)){ puts("Twin snowflakes found."); return 0; } } puts("No two snowflakes are alike."); return 0; }View Code
(5)最小循環元
直接上柿子:
①如果(i-next[i])%i==0,那麽(i-next[i])是s[1~i]的最小循環元長度.
②一個字符串的所有循環元長度都是最小循環元長度的倍數.
③把一個任意的s在添加字符最少的情況下補成一個循環同構串s‘,設s的長度為len,那麽有兩種情況:
(i)next[i]=0 此時s自成s‘的最小循環元,即需添加len個字符才能補成循環同構串.
(ii)next[i]≠0 此時s’的最小循環元長度為t=(len-next[len]),若(len%t==0),s就等於s‘,否則最少需添加(t-len%t)個字符才能補成循環同構串.
(6)Trie
這可能是最好理解的字符串算法之一了。
註意開數組時節點數應為(串的個數*串的最長長度)。
來個一般的模板:(HDU-1671)
#include<cstdio> #include<cctype> #include<cstring> #include<iostream> using namespace std; const int N=1e5+5; inline int read(){ int x=0,w=0;char ch=0; while(!isdigit(ch)) w|=ch==‘-‘,ch=getchar(); while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return w?-x:x; } struct Trie{ int tot; bool mark[N]; int node[N][15]; void reset(){ tot=0; memset(mark,0,sizeof mark); memset(node,0,sizeof node); } bool insert(char *s){ int p=0; int l=strlen(s); for(int i=0;i<l;++i){ int num=s[i]^48; if(!node[p][num]) node[p][num]=++tot; p=node[p][num]; if(mark[p]) return false; } mark[p]=true; for(int i=0;i<=9;++i) if(node[p][i]) return false; return true; } }T; int t,n; char s[10005]; int main(){ t=read(); while(t--){ T.reset(); bool fg=0; n=read(); for(int i=1;i<=n;++i){ scanf("%s",s); if(fg) continue; if(T.insert(s)) continue; else fg=1; } if(fg) puts("NO"); else puts("YES"); } return 0; }View Code
稍稍擴展一下,可以用Trie來搞異或和的問題。
看這道題:POJ-3764
建一棵以0為根的樹,用d[i]表示從根節點到節點i路徑上的異或和,可以知道對於任意兩個節點i和j,它們之間路徑的異或和就等於d[i]^d[j]。
那麽問題轉化成:在數組d中找兩個數,使它們的異或值最大。
這可以用Trie解決。具體來說,把每個數視為32位的二進制串(不足在高位補0),建一棵01Trie樹,然後對於每個數,在Trie樹中盡量走與它的每一位相反的邊(具體看代碼)。
要註意高位補0是如何實現的,還有節點數應是(數的個數*32)。
#include<cstdio> #include<cctype> #include<cstring> #include<algorithm> using namespace std; const int N=1e5+5; inline int read(){ int x=0,w=0;char ch=0; while(!isdigit(ch)) w|=ch==‘-‘,ch=getchar(); while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar(); return w?-x:x; } struct Graph{ struct Edge{ int v,w,last; }e[N<<1]; int tot,tail[N]; void clear(){ tot=0; memset(tail,0,sizeof tail); } void add(int x,int y,int z){ e[++tot]=(Edge){y,z,tail[x]}; tail[x]=tot; } }G; int n,d[N]; void dfs(int x,int pre){ for(int p=G.tail[x];p;p=G.e[p].last){ int v=G.e[p].v,w=G.e[p].w; if(v^pre){ d[v]^=d[x]^w; dfs(v,x); } } } int tree[N<<5][2],tot; void insert(int x){ int p=1; for(int i=30;i>=0;--i){ int ch=(x>>i)&1; if(!tree[p][ch]){ tree[p][ch]=++tot; } p=tree[p][ch]; } } int search(int x){ int p=1,ans=0; for(int i=30;i>=0;--i){ int ch=(x>>i)&1; if(tree[p][ch^1]) p=tree[p][ch^1],ans|=(1<<i); else p=tree[p][ch]; } return ans; } int main(){ while(scanf("%d",&n)!=EOF){ int ans=0; G.clear();tot=1; memset(d,0,sizeof d); memset(tree,0,sizeof tree); for(int i=1;i<n;++i){ int x=read(),y=read(),z=read(); G.add(x,y,z),G.add(y,x,z); } dfs(0,-1); for(int i=0;i<n;++i){ ans=max(ans,search(d[i])); insert(d[i]); } printf("%d\n",ans); } return 0; }View Code
再擴展一下,可以解決經典的哈夫曼編碼問題,在此就不說了(這個人太懶了)。
(7)AC自動機
至此,AC自動機終於揭開了“自動AC題目”的神秘面紗,向世人顯露真容(滑稽)
與以往想象的不同,其實AC自動機非常好理解也非常好寫。
掛一道幾乎是模板題的代碼:HDU-3695
#include<cstdio> #include<cstring> #include<queue> #include<iostream> using namespace std; const int N=255*1005; namespace AC{ //根節點是0 int mark[N]; int tree[N][26],fail[N],tot; void clear(){ tot=0; memset(mark,0,sizeof mark); memset(fail,0,sizeof fail); memset(tree,0,sizeof tree); } void insert(string s){ int p=0; for(int i=0;i<s.size();++i){ int ch=s[i]-‘A‘; if(!tree[p][ch]) tree[p][ch]=++tot; p=tree[p][ch]; } ++mark[p]; } void getfail(){ queue<int> q; for(int i=0;i<26;++i)if(tree[0][i]){ fail[tree[0][i]]=0; q.push(tree[0][i]); } while(!q.empty()){ int h=q.front();q.pop(); for(int i=0;i<26;++i) if(tree[h][i]){ fail[tree[h][i]]=tree[fail[h]][i]; q.push(tree[h][i]); } else tree[h][i]=tree[fail[h]][i]; } } int query(string s){ int ans=0,p=0; for(int i=0;i<s.size();++i){ p=tree[p][s[i]-‘A‘]; for(int j=p;j&&mark[j]^-1;j=fail[j]){ ans+=mark[j]; mark[j]=-1; } } return ans; } }; void turn(string &s){ string res; int i=0; while(i<s.size()){ if(s[i]^‘[‘) res+=s[i]; else{ int num=0;string tmp; while(s[i+1]>=‘0‘&&s[i+1]<=‘9‘){ num=num*10+(s[i+1]^48); ++i; } while(s[i+1]^‘]‘){ tmp+=s[i+1]; ++i; } ++i; while(num--) res+=tmp; } ++i; } s=res; } int n,t; string s,_s; int main(){ ios::sync_with_stdio(false); cin>>t; while(t--){ int ans=0; AC::clear(); _s.clear(); cin>>n; for(int i=1;i<=n;++i){ cin>>s; AC::insert(s); } AC::getfail(); cin>>s;turn(s); ans+=AC::query(s); for(int i=s.length()-1;i>=0;--i) _s+=s[i]; ans+=AC::query(_s); cout<<ans<<endl; } return 0; }View Code
(8)後綴數組
還沒學。。。留個坑。
【總結】字符串知識清單