字符串算法并不多,KMP,trie,AC自动机就是其中几个最经典的。字符串的题目灵活多变也有许多套路,需要多做题才能体会。这里收集了许多前辈的题目做个集合,方便自己回忆。
KMP题目:https://blog.csdn.net/qq_38891827/article/details/80501506
Trie树题目:https://blog.csdn.net/qq_38891827/article/details/80532462
AC自动机:模板https://www.luogu.org/blog/42196/qiang-shi-tu-xie-ac-zi-dong-ji
AC自动机题目集:https://www.cnblogs.com/kuangbin/p/3164106.html
KMP:
LuoguP3375 KMP模板
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=1000000+10; char a[N],b[N]; int n,m,nxt[N],f[N],g[N]; void get_next() { nxt[1]=0; for (int i=2,j=0;i<=n;i++) { while (j>0 && a[j+1]!=a[i]) j=nxt[j]; if (a[j+1]==a[i]) j++; nxt[i]=j; } } void KMP() { for (int i=1,j=0;i<=m;i++) { while (j>0 && (j==n || b[i]!=a[j+1])) j=nxt[j]; if (b[i]==a[j+1]) j++; f[i]=j; if (f[i]==n) g[i]=1; //这就是A在B中的某一次出现 } } int main() { scanf("%s",b+1); m=strlen(b+1); scanf("%s",a+1); n=strlen(a+1); get_next(); KMP(); for (int i=1;i<=m;i++) if (g[i]==1) cout<<i-n+1<<endl; for (int i=1;i<=n;i++) cout<<nxt[i]<<" "; return 0; }
HDU-2087
给出A串和B串,问B串在A串中出现次数。不能重叠!!!KMP裸题,允许重叠的话匹配成功时j=nxt[j],不允许重叠的话匹配成功时j=0。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=1e3+10; char a[N],b[N]; int n,m,nxt[N],f[N],g[N]; void get_next() { nxt[1]=0; for (int i=2,j=0;i<=n;i++) { while (j>0 && a[j+1]!=a[i]) j=nxt[j]; if (a[j+1]==a[i]) j++; nxt[i]=j; } } int KMP() { int ret=0; for (int i=1,j=0;i<=m;i++) { while (j>0 && (j==n || b[i]!=a[j+1])) j=nxt[j]; if (b[i]==a[j+1]) j++; f[i]=j; if (f[i]==n) ret++,j=0; //匹配成功:因为不允许重叠所以要把j=0 } return ret; } int main() { while (scanf("%s",b+1) && b[1]!='#') { memset(nxt,0,sizeof(nxt)); scanf("%s",a+1); n=strlen(a+1); m=strlen(b+1); get_next(); cout<<KMP()<<endl; } return 0; }
POJ-1961/HDU-1358
给出一个串,问每个前缀的最小循环节以及循环节大小。KMP经典题,对于s[1-i],最小循环节就是i-nxt[i],循环节大小就是i/(i-nxt[i])。
#include<iostream> #include<cstdio> using namespace std; const int N=1000000+10; char S[N]; int n,nxt[N],f[N]; void get_next() { nxt[1]=0; for (int i=2,j=0;i<=n;i++) { while (j>0 && S[j+1]!=S[i]) j=nxt[j]; if (S[j+1]==S[i]) j++; nxt[i]=j; } } int main() { int T=0; while (scanf("%d",&n) && n) { printf("Test case #%d\n", ++T); scanf("%s",S+1); get_next(); for (int i=2;i<=n;i++) if (i%(i-nxt[i])==0 && i/(i-nxt[i])>1) printf("%d %d\n",i,i/(i-nxt[i])); printf("\n"); } return 0; }
HDU-3746
添加最少的字符使原字符串变成周期至少为2的循环字符串。求出最小循环节lop,当且仅当n%lop==0 && n/lop>1才不需要添加,否则添加 lop-n%lop个字符。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=1000000+10; char S[N]; int n,nxt[N],f[N]; void get_next() { nxt[1]=0; for (int i=2,j=0;i<=n;i++) { while (j>0 && S[j+1]!=S[i]) j=nxt[j]; if (S[j+1]==S[i]) j++; nxt[i]=j; } } int main() { int T; scanf("%d",&T); while (T--) { scanf("%s",S+1); n=strlen(S+1); for (int i=0;i<=n;i++) nxt[i]=0; get_next(); int lop=n-nxt[n]; if (n%lop==0 && n/lop>1) puts("0"); else printf("%d\n",lop-n%lop); } return 0; }
POJ-2752
给定一个字符串,输出该串所有的前后缀相同的前缀位置。正好nxt数组就是前后缀相同大小,但是注意到nxt只记录了最大前后缀,但是题目要求输出所有前后缀,所以我们递归输出所以的nxt即可。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=4e5+10; char a[N],b[N]; int n,m,nxt[N],f[N],g[N]; void get_next() { nxt[1]=0; for (int i=2,j=0;i<=n;i++) { while (j>0 && a[j+1]!=a[i]) j=nxt[j]; if (a[j+1]==a[i]) j++; nxt[i]=j; } } void dfs(int x) { if (x==0) return; dfs(nxt[x]); printf("%d ",x); } int main() { while (scanf("%s",a+1)!=EOF) { n=strlen(a+1); for (int i=1;i<=n;i++) nxt[i]=0; get_next(); dfs(n); puts(""); } return 0; }
Trie:
HDU-1251
先给出一堆单词然后询问,每个询问输入一个字符串s问以s为前缀的单词个数。询问有多少个单词是s的前缀的话就只在单词结束点++,询问s是多少个单词的前缀的话就在单词每一个点都+1代表访问次数。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=1000000+10; int trie[2*N][26],tot=1; int n,m,endp[2*N]; void insert(char *str) { int p=1; for (int i=0;i<strlen(str);i++) { int x=str[i]-'a'; if (!trie[p][x]) trie[p][x]=++tot; p=trie[p][x]; endp[p]++; //访问次数 } //endp[p]++; //单词个数 } int search(char *str) { int p=1; for (int i=0;i<strlen(str);i++) { int x=str[i]-'a'; p=trie[p][x]; if (p==0) break; } return endp[p]; } int main() { char s[15]; bool ok=0; while (gets(s)) { if (s[0]=='\0') {ok=1; continue;} if (!ok) insert(s); else printf("%d\n",search(s)); } return 0; }
HDU-2072
问不同单词个数,一边查询只有没出现过的单词插入即可。
#include<bits/stdc++.h> using namespace std; const int N=1e6+10; int trie[2*N][26],tot=1; int n,m,endp[2*N]; string ss,s,str1; void insert(string str) { int p=1; for (int i=0;i<str.length();i++) { int x=str[i]-'a'; if (!trie[p][x]) trie[p][x]=++tot; p=trie[p][x]; } endp[p]++; //单词个数 } int search(string str) { int p=1; for (int i=0;i<str.length();i++) { int x=str[i]-'a'; p=trie[p][x]; if (p==0) break; } return endp[p]; } int main() { ios::sync_with_stdio(false); while(getline(cin,str1)) { if(str1=="#") break; stringstream ss(str1); int ans=0; while (ss>>s) { if (search(s)==0) ans++,insert(s); } printf("%d\n",ans); for (int i=1;i<=tot;i++) { endp[i]=0; for (int j=0;j<=26;j++) trie[i][j]=0; } tot=1; } return 0; }
POJ-2001
给一堆单词问每个单词独有的前缀。先把全部单词插入到trie树中,trie树的endp[i]代表该点被访问次数,询问时直到访问次数为1的就是该单词独有的。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=1000+10; int trie[20*N][26],tot=1; int n,m,num=1,endp[20*N]; char s[1010][25]; void insert(char *str) { int p=1; for (int i=0;i<strlen(str);i++) { int x=str[i]-'a'; if (!trie[p][x]) trie[p][x]=++tot; p=trie[p][x]; endp[p]++; } //endp[p]++; //单词个数 } int search(char *str) { int p=1,ans=0; for (int i=0;i<strlen(str);i++) { int x=str[i]-'a'; p=trie[p][x]; if (p==0) break; if (endp[p]==1) return i+1; } return strlen(str); } int main() { while (scanf("%s",s[num])!=EOF) { insert(s[num]); num++; } num--; for (int i=1;i<=num;i++,puts("")) { int tmp=search(s[i]); printf("%s ",s[i]); for (int j=0;j<tmp;j++) printf("%c",s[i][j]); } return 0; }
POJ-3630
给一堆字符串,问是否存在某个单词是某个单词的前缀。老规矩先把全部单词插到trie树,然后询问看每次单词是否有出现次数>1的单词。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N=10000+10; int trie[10*N][26],tot=1; int n,m,endp[10*N]; char s[N][12]; void insert(char *str) { int p=1; for (int i=0;i<strlen(str);i++) { int x=str[i]-'0'; if (!trie[p][x]) trie[p][x]=++tot; p=trie[p][x]; } endp[p]++; //单词个数 } int search(char *str) { int p=1,ans=0; for (int i=0;i<strlen(str);i++) { int x=str[i]-'0'; p=trie[p][x]; if (p==0) break; ans+=endp[p]; } return ans; } int main() { int T; cin>>T; while (T--) { scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%s",s[i]),insert(s[i]); bool ok=1; for (int i=1;i<=n;i++) if (search(s[i])>1) { ok=0; break; } printf("%s\n",ok?"YES":"NO"); for (int i=0;i<=tot;i++) { endp[i]=0; for (int j=0;j<=10;j++) trie[i][j]=0; } tot=1; } return 0; }
AC自动机:
洛谷P3808AC自动机模板1,求有多少个模式串在文本串里出现过。
#include<bits/stdc++.h> using namespace std; const int N=1000000+10; int n,cnt=0; char s[N]; struct node{ int val; int vis[30]; int fail; }ac[N]; void Trie_init() { cnt=0; memset(ac[0].vis,0,sizeof(ac[0].vis)); } int idx(char c) { return c-'a'; } void Insert(char *s) { int u=0,len=strlen(s); for (int i=0;i<len;i++) { int c=idx(s[i]); if (!ac[u].vis[c]) { ac[u].vis[c]=++cnt; ac[cnt].val=ac[cnt].fail=0; memset(ac[cnt].vis,0,sizeof(ac[cnt].vis)); } u=ac[u].vis[c]; } ac[u].val++; } queue<int> q; void Get_fail() { while (!q.empty()) q.pop(); for (int i=0;i<26;i++) { int u=ac[0].vis[i]; if (u) { ac[u].fail=0; q.push(u); } } while (!q.empty()) { int u=q.front(); q.pop(); for (int i=0;i<26;i++) { int v=ac[u].vis[i]; if (v) { ac[v].fail=ac[ac[u].fail].vis[i]; q.push(v); } else ac[u].vis[i]=ac[ac[u].fail].vis[i]; } } } int query(char *s) { int len=strlen(s); int u=0,ans=0; for (int i=0;i<len;i++) { u=ac[u].vis[idx(s[i])]; for (int t=u;t&&ac[t].val!=-1;t=ac[t].fail) { ans+=ac[t].val; ac[t].val=-1; } } return ans; } int main() { Trie_init(); scanf("%d",&n); for (int i=1;i<=n;i++) { scanf("%s",s); Insert(s); } Get_fail(); scanf("%s",s); cout<<query(s)<<endl; return 0; }
洛谷P3796AC自动机模板2,找出哪些模式串在文本串T中出现的次数最多并输出。
#include<bits/stdc++.h> using namespace std; const int N=1000000+10; int n,cnt=0,ans[155]; char p[N],s[155][100]; struct node{ int val; int vis[30]; int fail; }ac[N]; void Trie_init() { cnt=0; memset(ac[0].vis,0,sizeof(ac[0].vis)); } int idx(char c) { return c-'a'; } void Insert(char *s,int id) { int u=0,len=strlen(s); for (int i=0;i<len;i++) { int c=idx(s[i]); if (!ac[u].vis[c]) { ac[u].vis[c]=++cnt; ac[cnt].val=ac[cnt].fail=0; memset(ac[cnt].vis,0,sizeof(ac[cnt].vis)); } u=ac[u].vis[c]; } ac[u].val=id; } queue<int> q; void Get_fail() { while (!q.empty()) q.pop(); for (int i=0;i<26;i++) { int u=ac[0].vis[i]; if (u) { ac[u].fail=0; q.push(u); } } while (!q.empty()) { int u=q.front(); q.pop(); for (int i=0;i<26;i++) { int v=ac[u].vis[i]; if (v) { ac[v].fail=ac[ac[u].fail].vis[i]; q.push(v); } else ac[u].vis[i]=ac[ac[u].fail].vis[i]; } } } int query(char *s) { int len=strlen(s); int u=0,ret=0; for (int i=0;i<len;i++) { u=ac[u].vis[idx(s[i])]; for (int t=u;t&&ac[t].val!=-1;t=ac[t].fail) { if (ac[t].val) ret=max(ret,++ans[ac[t].val]); //ans[i]为模式串i的出现次数 } } return ret; //返回出现最多的次数 } int main() { while (scanf("%d",&n) && n) { Trie_init(); for (int i=1;i<=n;i++) { scanf("%s",s[i]); Insert(s[i],i); } Get_fail(); scanf("%s",p); memset(ans,0,sizeof(ans)); int Max=query(p); printf("%d\n",Max); for (int i=1;i<=n;i++) if (ans[i]==Max) printf("%s\n",s[i]); } return 0; }
洛谷P5357AC自动机模板3,求出每个模式串 Ti 在 S 中出现的次数。这一题的重复单词有影响,所以我们不能用上一题记录单词出现次数,而是应该计算该位置的单词出现次数,然后对于每个单词记录它在Trie树上的位置,输出。
到这里也还不能够获得AC,会TLE。洛谷题解上有几种优化的方式,我这里选择的是拓扑排序优化建图的方式。
来源:https://www.cnblogs.com/clno1/p/10986068.html