字符串(长期)
字符串#
序言#
字符串说实话我不算是很擅长,但是我还是想写一点东西。
字符串是一种存储字符的数据结构,本身来说这个并不难,但是因此也拓展出了非常非常多的算法。
很多人学习字符串的基本算法时就被劝退了,但殊不知这只是字符串的起点。
所以,坚持地学习下去吧,等你有一天层次高了后,你会发现:我以前怎么连这么弱智的算法都不会。
本人也是刚接触字符串不久,所以并不会涉及太难的知识点。
KMP#
KMP 是字符串匹配算法中的经典,它同时由三位作者提出,故它的名字就是这三个人的名字首字母。
KMP 的精髓就是一个失配数组
我们先正常遍历,若发现一个不匹配的,我们就回退到上一个匹配的位置,直到找到一个匹配的位置。
但这样做有一个问题,就是回退的位置太远了,我们可以用失配数组来优化这个过程。
我们可以发现,失配数组
那么,我们就可以根据失配数组来优化回退的位置。
这里因为笔者觉得 KMP 其实在省选难度并不会出现(一般都是 AC自动机或者马拉车等),所以就不展开讲解了。
这里放个模板
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
char a[1000010],b[1000010];
int nxt[1000010];
signed main(){
cin>>a+1;
cin>>b+1;
int j=0;
int la=strlen(a+1),lb=strlen(b+1);
for(int i=2;i<=lb;++i){
while(j && b[j+1]!=b[i]) j=nxt[j];
if(b[j+1]==b[i]) j++;
nxt[i]=j;
}
j=0;
for(int i=1;i<=la;++i){
while(j && b[j+1]!=a[i]) j=nxt[j];
if(b[j+1]==a[i]) j++;
if(j==lb){
cout<<i-lb+1<<endl;
j=nxt[j];
}
}
for(int i=1;i<=lb;++i) cout<<nxt[i]<<' ';
cout<<endl;
return 0;
}
trie树#
trie树是一种树形结构,用来存储字符串,它是一种非常经典的字符串匹配算法。
trie树的每个节点都是一个字符,如果当前字符不存在,则创建一个新的节点,如果存在,则进入这个节点。
具体来说,我们先判断当前节点
然后,我们要查询一个字符串是否在trie树中,我们从根节点开始,沿着树边向下,如果当前节点的儿子中有某个儿子的字符与查询字符串的当前字符相同,则继续沿着这个儿子的边向下,如果没有,则说明这个字符不在trie树中,返回false。
如果查询到最后一个字符,则说明这个字符串在trie树中,返回true。
trie树的插入以及查询时间复杂度均为
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
struct node{
int a[72];
int flag;
}t[3000010];
string a;
int n,m;
int u,cnt=0;
int get(char a){
if(a>='A' && a<='Z') return (int)(a-'A');
else if(a>='a' && a<='z') return (int)(a-'a'+26);
else return (int)(a-'0'+52);
}
void insert(string s){
int len=s.size();
u=0;
s='0'+s;
for(int i=1;i<=len;++i){
int k=get(s[i]);
if(!t[u].a[k]) t[u].a[k]=++cnt;
u=t[u].a[k];
t[u].flag++;
}
}
int query(string s){
int len=s.size();
u=0;
s='0'+s;
for(int i=1;i<=len;++i){
int k=get(s[i]);
if(!t[u].a[k]) return 0;
u=t[u].a[k];
}
return t[u].flag;
}
signed main(){
auto solve=[&](){
read(n),read(m);
for(int i=0;i<=cnt;++i) for(int j=0;j<=70;++j) t[i].a[j]=0;
for(int i=0;i<=cnt;++i) t[i].flag=0;
cnt=0;
for(int i=1;i<=n;++i){
cin>>a;
insert(a);
}
for(int i=1;i<=m;++i){
cin>>a;
cout<<query(a)<<endl;
}
};
//freopen(.in,'r',stdin);
//freopen(.out,'w',stdout);
read(T);
while(T--) solve();
return 0;
}
AC自动机#
AC自动机是一种字符串匹配算法,它是基于trie树的改进,它可以解决一些trie树无法解决的问题。
AC自动机可以说是 KMP 与 trie树 的结合体,它在trie树的基础上,增加了一些状态转移的规则,使得它可以更好地匹配字符串。
具体来说,KMP 只能处理匹配单个字符串的问题,一旦有
我们还是对每个字符串建 trie树,然后我们需要一个链接边,类似 KMP 的,它的目标我们设为
AC自动机的状态转移规则如下:
- 如果当前字符与当前节点的字符相同,则进入当前节点的儿子,并将当前节点设为
; - 如果当前字符与当前节点的字符不同,则沿着当前节点的链接边,找到第一个与当前字符相同的节点,并将当前节点设为
; - 如果当前字符与当前节点的字符相同,但当前节点的儿子中没有与当前字符相同的儿子,则说明当前字符不在当前节点的子树中,则沿着当前节点的链接边,找到第一个与当前字符相同的节点,并将当前节点设为
;
这样,我们就可以根据
AC自动机的插入与查询时间复杂度均为
代码(二次加强版)如下:
#include<bits/stdc++.h>
#define ull unsigned long long
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
struct node{
int a[27];
int flag;
int ans;
node(){
memset(a,0,sizeof(a));
flag=0;
ans=0;
}
}trie[2000010];
int n;
string t;
string s;
int u,cnt=0;
int fail[2000010],vis[200010],in[20000010],pp[2000001];
int get(char a) {return (int)(a-'a');}
queue<int> q;
void insert(string s,int id){
int len=s.size();
s='0'+s;
u=0;
for(int i=1;i<=len;++i){
int k=get(s[i]);
if(!trie[u].a[k]) trie[u].a[k]=++cnt;
u=trie[u].a[k];
}
if(!trie[u].flag) trie[u].flag=id;
pp[id]=trie[u].flag;
}
void build(){
for(int i=0;i<26;++i) if(trie[0].a[i]) q.push(trie[0].a[i]);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;++i){
int v=trie[u].a[i];
if(v) fail[v]=trie[fail[u]].a[i],in[fail[v]]++,q.push(v);
else trie[u].a[i]=trie[fail[u]].a[i];
}
}
}
void topo(){
for(int i=1;i<=cnt;++i) if(in[i]==0) q.push(i);
while(!q.empty()){
int u=q.front();
q.pop();
vis[trie[u].flag]=trie[u].ans;
int v=fail[u];in[v]--;
trie[v].ans+=trie[u].ans;
if(in[v]==0) q.push(v);
}
}
void query(string s){
int len=s.size();
s='0'+s;
u=0;
for(int i=1;i<=len;++i){
int p=get(s[i]);
u=trie[u].a[p];
trie[u].ans++;
}
}
signed main(){
auto solve=[&](){
read(n);
for(int i=1;i<=n;++i) cin>>t,insert(t,i);
build();
cin>>s;
query(s);
topo();
for(int i=1;i<=n;++i) cout<<vis[pp[i]]<<endl;
};
//freopen(.in,'r',stdin);
//freopen(.out,'w',stdout);
//read(T);
while(T--) solve();
return 0;
}
拓展KMP(Z函数)#
其实这个我不是很理解与 KMP 有什么关系,但是这个我印象比较深。
Z函数的精髓是维护一个盒子,
简单点理解就是:当前
说以来比较绕口,举个例子:
我们先不管
我们假设
那么也有:
显然可以观察到,我们拓展
但是我们要考虑一个问题,有可能
所以我们先不考虑超出的部分,那么
现在考虑超出的部分或者本身
while(a[z[i]]==a[i+z[i]] && i+z[i]<n) z[i]++;
最后我们还要移动
所以当当前的
即:
if(i+z[i]>r) l=i,r=i+z[i];
这样就得到了
但是其实我到现在都没有遇到需要用 不如后缀自动机)。
代码如下:
void getz(string a){
n=a.size();
for(int i=1,l=0,r=0;i<n;++i){
if(i<r) z[i]=min(z[i-l],r-i);
while(a[z[i]]==a[i+z[i]] && i+z[i]<n) z[i]++;
if(i+z[i]>r) l=i,r=i+z[i];
}
}
Manacher马拉车#
求解回文子串的一种算法,用的不多,一般都用回文自动机(PAM)或者后缀数组啥的。
其实跟
代码也是高度相似:
void manacher(char *s,int n){
d[1]=1;
for(int i=2,l,r=1;i<=n;++i){
if(i<=r) d[i]=min(d[r-i+l],r-i+1);
while(s[i-d[i]]==s[i+d[i]]) d[i]++;
if(i+d[i]-1>r) l=i-d[i]+1,r=i+d[i]-1;
}
}
建议读者自己理解一下,理解不了的话建议去 Bilibili找董晓算法学习。
后缀数组(SA)#
现在我只学习了后缀数组排序算法,实际运用还没学。
后缀排序可以在
具体思路就是倍增一个长度
排序的过程就是基数排序,没学的自己去学,我不讲基础的。
同时,还可以求出 LCP 数组,即
但是在模板中并没有对
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
P res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
res=res*10+ch-'0';
ch=getchar();
}
x=res*f;
}
int T=1;
const int N=2e6+10;
char s[N];
int n,m;
int x[N],y[N],sa[N],rk[N],h[N];
int c[N];
void getsa(){
int i,k;
for(i=1;i<=n;++i) c[x[i]=s[i]]++;
for(i=1;i<=m;++i) c[i]+=c[i-1];
for(i=n;i>=1;--i) sa[c[x[i]]--]=i;
for(k=1;k<=n;k<<=1){
//第二关键字排序
memset(c,0,sizeof(c));
for(i=1;i<=n;++i) y[i]=sa[i];
for(i=1;i<=n;++i) c[x[y[i]+k]]++;
for(i=1;i<=m;++i) c[i]+=c[i-1];
for(i=n;i>=1;--i) sa[c[x[y[i]+k]]--]=y[i];
//第一关键字排序
memset(c,0,sizeof(c));
for(i=1;i<=n;++i) y[i]=sa[i];
for(i=1;i<=n;++i) c[x[y[i]]]++;
for(i=1;i<=m;++i) c[i]+=c[i-1];
for(i=n;i>=1;--i) sa[c[x[y[i]]]--]=y[i];
//重排
for(i=1;i<=n;++i) y[i]=x[i];
for(m=0,i=1;i<=n;++i){
if(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=m;
else x[sa[i]]=++m;
if(m==n) break;
}
}
}
void geth(){
for(int i=1;i<=n;++i) rk[sa[i]]=i;
for(int i=1,k=0;i<=n;++i){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n && j+k<=n && s[i+k]==s[j+k]) k++;
h[rk[i]]=k;
}
}
signed main(){
scanf("%s",s+1);
n=strlen(s+1);m=122;
getsa();
// geth();
for(int i=1;i<=n;++i) printf("%d ",sa[i]);
printf("\n");
return 0;
}
作者:God_Max_Me
出处:https://www.cnblogs.com/lizihan00787/p/18361413
版权:本作品采用「God_Max_Me-非商业性使用」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端