之前一直跳过了字符串,现在才开始系统地学习,感觉需要记得模板挺多,在这里列个知识清单总结一下。
(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> #include<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 ",++cas,Manacher(s)); } return 0; }
(3)KMP
这个算法我起码学了3遍,还是经常忘。。。
这份代码s下标从1开始。
#include<cstring> #include<cstdio> #include<iostream> using namespace std; const int N=1e6+5; char A[N],B[N]; int la,lb,nxt[N]; void getnxt(){ nxt[1]=0; for(int i=2,j=0;i<=la;++i){ while(j&&A[i]^A[j+1]) j=nxt[j]; if(A[i]==A[j+1]) ++j; nxt[i]=j; } } int kmp(){ int ans=0; for(int i=1,j=0;i<=lb;++i){ while(j&&B[i]^A[j+1]) j=nxt[j]; if(B[i]==A[j+1]) ++j; if(j==la) ++ans,j=nxt[j]; } return ans; } int t; int main(){ cin>>t; while(t--){ scanf("%s %s",A+1,B+1); la=strlen(A+1),lb=strlen(B+1); getnxt(); printf("%d ",kmp()); } return 0; }
(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); }
给一道题目: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; }
(5)最小循环元
直接上结论:
①如果i%(i-next[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; }
稍稍扩展一下,可以用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 ",ans); } return 0; }
再扩展一下,可以解决经典的哈夫曼编码问题,在此就不说了(这个人太懒了)。
(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; }
还有一道题:洛谷P3796-AC自动机(加强版) 里面我加了点优化(lst数组),就是把fail指针跳跃的路径给压缩了,实测时间可减少四分之一。
#include<cstdio> #include<queue> #include<cstring> #include<iostream> using namespace std; const int N=155,M=26; int n; string str,s[N]; namespace AC{ int tot=0,fail[N*70],lst[N*70],mark[N*70],cnt[N],tree[N*70][M]; void clear(){ tot=0; memset(lst,0,sizeof lst); memset(cnt,0,sizeof cnt); memset(fail,0,sizeof fail); memset(tree,0,sizeof tree); memset(mark,0,sizeof mark); } void insert(string &s,int id){ int p=0; for(int i=0;i<s.length();++i){ if(!tree[p][s[i]-'a']) tree[p][s[i]-'a']=++tot; p=tree[p][s[i]-'a']; } mark[p]=id; } void getfail(){ queue<int> q; for(int i=0;i<M;++i)if(tree[0][i]) //别忘了if lst[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<M;++i) if(!tree[h][i]) tree[h][i]=tree[fail[h]][i]; else{ fail[tree[h][i]]=tree[fail[h]][i], lst[tree[h][i]]=mark[fail[tree[h][i]]]?fail[tree[h][i]]:lst[fail[tree[h][i]]]; q.push(tree[h][i]); } } } void query(string &str){ int maxl=0,p=0; for(int i=0;i<str.length();++i){ p=tree[p][str[i]-'a']; for(int j=p;j&&~mark[j];j=lst[j]) ++cnt[mark[j]]; } for(int i=1;i<=n;++i) maxl=max(maxl,cnt[i]); cout<<maxl<<endl; for(int i=1;i<=n;++i) if(cnt[i]==maxl) cout<<s[i]<<endl; } } int main(){ while(cin>>n&&n){ AC::clear(); for(int i=1;i<=n;++i){ cin>>s[i]; AC::insert(s[i],i); } AC::getfail(); cin>>str; AC::query(str); } return 0; }
(8)后缀数组
还没学。。。留个坑。