重修字符串
之前学得太屑了,现在重修一下。
一.普通字典树(trie):
1.效用:
维护一个字符串的集合,能够高效地插入新的字符串到集合中,也能高效地查找某个字符串或某个前缀是否在集合中。
2.实现:
(1):如何插入一个新的字符串,insert。
从根节点一直往下匹配,若无此字母,则新开一个关于此字母的节点,若匹配完成,则给当前节点标记末尾。
inline void ins(string s)
{
int x=0,len=s.size();
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(!ch[x][c])
{
cnt++;
ch[x][c]=cnt;
}
x=ch[x][c];
}
end[x]=true;
return;
}
(2):如何查询一个字符串是否在字典树中,query(ask)。
直接扫一遍,查询是否有,若无直接返回不。
inline bool ask(string s)
{
int x=0,len=s.size();
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(ch[x][c]==0)
return false;
x=ch[x][c];
}
return end[x];
}
典型例题:
例一 P2580 于是他错误的点名开始了
经典的 \(\text{trie}\) 树板子,就是要注意一下在哪里记录。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
inline int read()
{
register int x=0,f=1;
register char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-')
f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int ch[MAXN][27],cnt;
int en[MAXN];
int vis[MAXN];
inline void ins(string s)
{
int x=0;
int len=s.size();
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(!ch[x][c])
{
cnt++;
ch[x][c]=cnt;
}
x=ch[x][c];
}
en[x]=1;
return;
}
inline int ask(string s)
{
int x=0,len=s.size();
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(!ch[x][c])
return 3;
x=ch[x][c];
}
if(!en[x])
return 3;
if(!vis[x])
{
vis[x]++;
return 1;
}
return 2;
}
int n,m;
string s;
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(register int i=1;i<=n;i++)
cin>>s,ins(s);
cin>>m;
for(register int i=1;i<=m;i++)
{
cin>>s;
int ans=ask(s);
if(ans==1)
{
printf("OK\n");
continue;
}
else if(ans==2)
{
printf("REPEAT\n");
continue;
}
else if(ans==3)
{
printf("WRONG\n");
continue;
}
}
return 0;
}
例二 SP4033 PHONELST-Phone List
\(2\) 倍经验:UVA11362 Phone List(一毛一样)
\(2.5\) 倍经验:UVA644 Immediate Decodability(基本一样)
若当前插入的是 \(s\):
情况一:别的串是 \(s\) 的前缀,那么 \(s\) 到遍历结束之前,一定至少有一个染色的点。
情况二:\(s\) 是别的串的前缀,那么 \(s\) 的插入过程不会有任何新的节点产生,只需要判断 \(tot\) 是否没变。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
inline int read()
{
register int x=0,f=1;
register char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-')
f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int ch[MAXN][27],cnt;
int en[MAXN];
inline bool solve(string s)
{
int x=0,len=s.size();
bool flag=true;
for(register int i=0;i<len;i++)
{
int c=s[i]-'0';
if(!ch[x][c])
{
cnt++;
ch[x][c]=cnt;
flag=false;
}
x=ch[x][c];
if(en[x])
return true;
}
en[x]=true;
if(flag)
return true;
return false;
}
int t,n;
string s;
bool vis;
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>t;
while(t--)
{
memset(ch,0,sizeof(ch));
memset(en,0,sizeof(en));
vis=false;
cnt=0;
cin>>n;
for(register int i=1;i<=n;i++)
{
cin>>s;
if(solve(s))
vis=true;
}
if(vis)
{
printf("NO\n");
continue;
}
else
{
printf("YES\n");
continue;
}
}
return 0;
}
例三 P3879 阅读理解
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+10;
const int N=1005;
int ch[MAXN][27],cnt;
string s;
bitset<N>ed[MAXN];
int t,n,m;
inline void insert(int x,string s)
{
int x=0;
int len=s.size();
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(!trie[x][c])
trie[x][c]=++cnt;
x=trie[x][c];
}
ed[x][c]=true;
}
inline void ask(string s)
{
int x=0;
bool flag=1;
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(ch[x][c]==0)
{
flag=0;
break;
}
x=ch[x][c];
}
if(flag)
for(register int i=1;i<=n;i++)
if(ed[x][i])
printf("%d ",i);
puts("");
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(register int i=1;i<=n;i++)
{
cin>>t;
for(register int j=1;j<=t;j++)
{
cin>>s;
insert(i,s);
}
}
cin>>m;
for(register int i=1;i<=m;i++)
{
cin>>s;
ask(s);
}
return 0;
}
例四 P2922 Secret Message G
讲实话,题面有点糊。
我们来分析一下:
1.\(M\) 条信息,\(N\) 条暗号。
2.\(1\) 条信息是 \(1\) 条暗号的前缀,或\(1\) 条暗号是 \(1\) 条信息的前缀。
那么做法就很显然了。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
const int N=1e4+5;
int m,n,ch[MAXN][2],cnt;
int k,sum[MAXN],ed[MAXN];
inline void ins(int t,int p[])
{
int x=0;
for(register int i=1;i<=t;i++)
{
if(!ch[x][p[i]])
ch[x][p[i]]=++cnt;
x=ch[x][p[i]];
sum[x]++;
}
ed[x]++;
return;
}
inline int ask(int t,int p[])
{
int x=0,ans=0;
for(register int i=1;i<=t;i++)
{
if(ch[x][p[i]]==0)
return ans;
x=ch[x][p[i]];
ans+=ed[x];
}
return ans-ed[x]+sum[x];
}
int p[MAXN];
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>m>>n;
for(register int i=1;i<=m;i++)
{
cin>>k;
for(register int i=1;i<=k;i++)
cin>>p[i];
ins(k,p);
}
for(register int i=1;i<=n;i++)
{
cin>>k;
for(register int i=1;i<=k;i++)
cin>>p[i];
printf("%d\n",ask(k,p));
}
return 0;
}
例五 P2292 L语言
这题卡空间真恶心
这道题虽然正解是 AC 自动机,题解区都是 AC 自动机的解法,但作为一个专业整活人,我决定来尝试一下 \(\text{trie}\) 树的写法。
首先我们可以想到在 \(\text{trie}\) 树上做 \(\text{dfs}\),但很显然 这样的复杂度是 \(O(n·len)\),前 \(80\%\) 能轻松过,但更新的数据会被卡掉。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
const int M=26;
bool en[N],vis[N<<1];
int ch[N][M],cnt,ans;
string ss;
inline void insert(string s)
{
int x=0,len=s.size();
for(register int i=0;i<len;i++)
{
int c=s[i]-'a';
if(ch[x][c]==0)
ch[x][c]=++cnt;
x=ch[x][c];
}
en[x]=true;
return;
}
inline void dfs(int pos, int cur)
{
int c=ss[pos]-'a';
cur=ch[cur][c];
if(cur)
dfs(pos+1,cur);
if(en[cur]==true)
{
ans=max(pos+1,ans);
if(vis[pos]==false)
dfs(pos+1,0);
vis[pos]=true;
}
return;
}
int n,m;
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(register int i=1;i<=n;i++)
{
string word;
cin>>word;
insert(word);
}
for(register int i=1;i<=m;i++)
{
memset(vis,false,sizeof(vis));
ans=0;
cin>>ss;
dfs(0,0);
cout<<ans<<endl;
}
return 0;
}
二.01字典树(01trie):
1.定义:
一颗只含 \(01\) 串的字典树,用于插入一些转化成二进制的整数。
2.实现:
insert函数:
inline void ins(int pos)
{
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(!ch[x][c])
ch[x][c]=++cnt;
x=ch[x][c];
}
return;
}
ask函数:
inline int ask(int pos)
{
int ans=0;
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(ch[x][c^1])
{
ans+=(c^1)<<i;
x=ch[x][c^1];
}
else
{
ans+=c<<i;
x=ch[x][c];
}
}
return ans;
}
典型例题:
例一 HDU4825 Xor Sum
思路:
1.将 \(n\) 个正整数转化成二进制并插入 \(\text{trie}\) 树;
2.询问时将一个整数 \(x\) 转化成二进制从根节点(高位)开始搜索,尽量取与 \(x\) 位上数值相反的数。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e6+5;
int ch[MAXN][2];
int cnt;
inline void ins(int pos)
{
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(!ch[x][c])
ch[x][c]=++cnt;
x=ch[x][c];
}
return;
}
inline int ask(int pos)
{
int ans=0;
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(ch[x][c^1])
{
ans+=(c^1)<<i;
x=ch[x][c^1];
}
else
{
ans+=c<<i;
x=ch[x][c];
}
}
return ans;
}
inline void init()
{
memset(ch,0,sizeof(ch));
cnt=0;
return;
}
int t;
int n,m;
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>t;
for(register int i=1;i<=t;i++)
{
cin>>n>>m;
init();
for(register int j=1;j<=n;j++)
{
int x;
cin>>x;
ins(x);
}
printf("Case #%d:\n",i);
for(register int j=1;j<=m;j++)
{
int x;
cin>>x;
printf("%d\n",ask(x));
}
}
return 0;
}
例二 HDU5536 Chip Factory
Code
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e3+5;
int a[MAXN];
int ch[MAXN*35][2],cnt;
int num[MAXN*35];
inline void ins(int pos,int val)
{
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(!ch[x][c])
ch[x][c]=++cnt;
x=ch[x][c];
num[x]+=val;
}
return;
}
inline int ask(int pos)
{
int x=0,ans=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(ch[x][c^1] && num[ch[x][c^1]])
{
ans|=1<<i;
x=ch[x][c^1];
}
else
x=ch[x][c];
}
return ans;
}
inline void init()
{
memset(ch,0,sizeof(ch));
memset(num,0,sizeof(num));
cnt=0;
return;
}
int t;
int n;
int ans;
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>t;
while(t--)
{
cin>>n;
ans=-1;
init();
for(register int i=1;i<=n;i++)
cin>>a[i];
for(register int i=1;i<=n;i++)
ins(a[i],1);
for(register int i=1;i<=n;i++)
{
ins(a[i],-1);
for(register int j=i+1;j<=n;j++)
{
ins(a[j],-1);
ans=max(ans,ask(a[i]+a[j]));
ins(a[j],1);
}
ins(a[i],1);
}
printf("%lld\n",ans);
}
return 0;
}
例三 P4551 最长异或路径
思路:
1.定义 \(dp_a\) 表示从根节点到点 \(a\) 的路径上的边权异或和。
2. \(\text{dfs}\) 维护每个点 \(x\) 的 \(dp_x\)。
3.将 \(dp_x\) 依次询问最大的异或值,再插入 \(01\) \(\text{trie}\) 树。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
struct edge
{
int to,nxt,len;
}e[MAXN<<1];
int head[MAXN],cnt;
inline void add(int x,int y,int z)
{
e[++cnt].to=y;
e[cnt].len=z;
e[cnt].nxt=head[x];
head[x]=cnt;
return;
}
int n,tot,ch[MAXN*32][2];
int sum[MAXN];
inline void dfs(int x,int fa)
{
for(register int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,z=e[i].len;
if(y==fa)
continue;
sum[y]^=sum[x];
sum[y]^=z;
dfs(y,x);
}
return;
}
inline void ins(int pos)
{
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(!ch[x][c])
ch[x][c]=++tot;
x=ch[x][c];
}
return;
}
inline int ask(int pos)
{
int ans=0;
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(ch[x][c^1])
{
ans+=1<<i;
x=ch[x][c^1];
}
else
x=ch[x][c];
}
return ans;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(register int i=1;i<=n-1;i++)
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
add(y,x,z);
}
dfs(1,0);
for(register int i=1;i<=n;i++)
ins(sum[i]);
int ans=0;
for(register int i=1;i<=n;i++)
ans=max(ans,ask(sum[i]));
printf("%d\n",ans);
return 0;
}
例四 CF842D Vitya and Strange Lesson
考虑常用的操作异或集合的手段,我的第一反应是线性基,然而线性基只能插入,不能修改,再者她也不能查询 \(mex\) 这么奇怪的东西。
先不考虑异或,显然 \(mex\) 是有可二分性的,我们需要一个数据结构支持查询小于某个数的数是否都出现过,然后我们就可以二分了,我的第一反应是值域线段树,显然是可以做的,直接在线段树上二分就可以了,具体来说走到线段树的某个节点时,看一下左儿子存在的数是不是等于左儿子对应的值域大小,即看一下左儿子是不是满的。然而,线段树也不能维护异或这么奇怪的东西。
实际上我们可以选取和线段树结构相同的另一个数据结构(或者说她们就是同一个东西),\(01\) \(\text{trie}\)。
我们也可以在 \(01\) \(\text{trie}\) 上二分查 \(mex\),并且走到 \(\text{trie}\) 上某个点时,如果异或的数这一位为 \(1\),意义就是交换 \(\text{trie}\) 的左右儿子,然后就可以用和线段树一样的方法做了。
有一个细节就是,在计算 \(\text{trie}\) 一个点的 \(\text{size}\) 时,注意一个数出现多次只计算一次。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;
int ch[MAXN*32][2],cnt;
int siz[MAXN*32];
int tag;
int n,m;
inline void ins(int pos)
{
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(!ch[x][c])
ch[x][c]=++cnt;
x=ch[x][c];
}
return;
}
inline int ask(int pos)
{
int x=0;
int ans=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(siz[ch[x][c]]==(1<<(i+1))-1)
{
ans+=1<<i;
x=ch[x][c^1];
}
else
x=ch[x][c];
}
return ans;
}
inline void dfs(int x)
{
siz[x]=1;
for(register int i=0;i<=1;i++)
{
int y=ch[x][i];
if(!y)
continue;
dfs(y);
siz[x]+=siz[y];
}
return;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(register int i=1;i<=n;i++)
{
int x;
cin>>x;
ins(x);
}
dfs(0);
for(register int i=1;i<=m;i++)
{
int x;
cin>>x;
tag^=x;
printf("%d\n",ask(tag));
}
return 0;
}
例五 CF817E Choosing The Commander
思路:
明显是一个 \(01 \text{trie}\) 处理插入删除操作的题目,只是多了一个查询操作。
对于插入删除操作,直接弄一个 \(\text{cnt}\) 数组来记录插入和删除操作,然后套板子即可,不再赘述。
然后考虑询问。
我们可以做出这样的操作,对于当前讨论的这一位:
- 若 \(l=1\),则统计 \(k=0\) 子树上的答案,并向 \(k=1\) 的子树继续遍历,因为此时可以保证整个左子树都小于 \(l_i\)。
- 若 \(l=0\),则向 \(k=1\) 的子树继续遍历。
然后这题还要注意的一点是,它的异或 \(\text{tag}\) 并不是累积的,所以每次要清空。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e6+5;
int ch[MAXN][2],cnt;
int num[MAXN];
int n;
inline void ins(int pos,int val)
{
int x=0;
for(register int i=31;i>=0;i--)
{
int c=(pos>>i)&1;
if(!ch[x][c])
ch[x][c]=++cnt;
x=ch[x][c];
num[x]+=val;
}
return;
}
inline int ask(int pos,int tag)
{
int x=0;
int ans=0;
for(register int i=31;i>=0;i--)
{
int t=(pos>>i)&1,c=(tag>>i)&1;
if(t==1)
{
ans+=num[ch[x][c]];
x=ch[x][c^1];
}
else
x=ch[x][c];
if(x==0)
return ans;
}
return ans;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(register int i=1;i<=n;i++)
{
int op,x;
cin>>op>>x;
if(op==1)
ins(x,1);
if(op==2)
ins(x,-1);
if(op==3)
{
int y;
int tag=0;
cin>>y;
tag^=x;
printf("%d\n",ask(y,tag));
}
}
return 0;
}
三.可持久化01 trie:
可持久化01 trie模板:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5;
int cnt[MAXN];
int ch[MAXN][2],s[MAXN];
inline void ins(int now,int ls,int x,int p)//now为当前版本节点,ls为上个版本对应节点(rt[i]与rt[i-1])
{
if(p<0) return;
int t=(x>>p)&1;//取出第p位
ch[now][!t]=ch[ls][!t];//与上一个版本相连
ch[now][t]=++cnt;//给新节点
s[ch[now][t]]=s[ch[ls][t]]+1;//记录在前缀中出现次数
ins(ch[now][t],ch[ls][t],x,p-1);
return;
}
inline int ask(int l,int r,int x,int p)//l为左边界,r为右边界
{
if(p<0) return 0;
int t=(x>>p)&1;
if(s[ch[r][!t]]>s[ch[l][!t]]) return (1<<p)+ask(ch[l][!t],ch[r][!t],x,p-1);
else return ask(ch[l][t],ch[r][t],x,p-1);
}
典型例题
例一 P4735 最大异或和
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e7+5;
int n,m;
int cnt;
int ch[MAXN][2],s[MAXN];
int rt[MAXN];
int sum[MAXN];
inline void insert(int now,int ls,int x,int p)
{
if(p<0) return;
int t=(x>>p)&1;
ch[now][!t]=ch[ls][!t];
ch[now][t]=++cnt;
s[ch[now][t]]=s[ch[ls][t]]+1;
insert(ch[now][t],ch[ls][t],x,p-1);
return;
}
inline int ask(int l,int r,int x,int p)
{
if(p<0) return 0;
int t=(x>>p)&1;
if(s[ch[r][!t]]>s[ch[l][!t]]) return (1<<p)+ask(ch[l][!t],ch[r][!t],x,p-1);
else return ask(ch[l][t],ch[r][t],x,p-1);
}
int main()
{
scanf("%d%d",&n,&m);
rt[0]=++cnt;
insert(rt[0],0,0,23);
for(register int i=1;i<=n;i++)
{
int op;
scanf("%d",&op);
sum[i]=sum[i-1]^op;
rt[i]=++cnt;
insert(rt[i],rt[i-1],sum[i],23);
}
for(register int i=1;i<=m;i++)
{
char op;
int l,r,k;
cin>>op;
if(op=='A')
{
scanf("%d",&k);
n++;
sum[n]=sum[n-1]^k;
rt[n]=++cnt;
insert(rt[n],rt[n-1],sum[n],23);
}
else
{
scanf("%d%d%d",&l,&r,&k);
l--,r--;
printf("%d\n",ask(rt[l-1],rt[r],k^sum[n],23));
}
}
}
例二 P5283 异或粽子
给你\(n\)个数字,然后让你给出\(k\)各不同的区间\([l,r]\),然求区间最大异或和。
我们想,\(S_i=a_1\ xor\ a_2...a_i\),
所以可以将区间的异或转为两个值异或。
可以拿\(01\ trie\)来乱搞一下。
因为其实从高位置低位,若让其尽可能大,则要让两个异或值尽可能大。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e6+5;
int n,k,rt[MAXN],cnt;
int a[MAXN],ans;
struct node
{
int ch[2],siz,id;
}t[MAXN*40];
inline void insert(int &now,int pre,int bit,int id,int val)
{
now=++cnt; t[now]=t[pre];t[now].siz++;
if(bit==-1){t[now].id=id;return;}
if((val>>bit)&1) insert(t[now].ch[1],t[pre].ch[1],bit-1,id,val);
else insert(t[now].ch[0],t[pre].ch[0],bit-1,id,val);
return;
}
inline int ask(int u,int v,int bit,int val)
{
if(bit==-1) return t[v].id;
int d=(val>>bit)&1;
if(t[t[v].ch[d^1]].siz-t[t[u].ch[d^1]].siz>0) return ask(t[u].ch[d^1],t[v].ch[d^1],bit-1,val);
return ask(t[u].ch[d],t[v].ch[d],bit-1,val);
}
struct Node
{
int l,r,x,id,val;
Node(int ll=0,int rr=0,int xx=0)
{
l=ll;
r=rr;
x=xx;
id=ask(rt[l-1],rt[r],31,a[x]);
val=a[x]^a[id-1];
}
};
inline bool operator<(const Node &a,const Node &b)
{
return a.val<b.val;
}
priority_queue<Node>pq;
signed main()
{
scanf("%lld%lld",&n,&k);
for(register int i=1;i<=n;i++)
{
int op;
scanf("%lld",&op);
a[i]=a[i-1]^op;
}
for(register int i=1;i<=n;i++)
{
rt[i]=rt[i-1];
insert(rt[i],rt[i],31,i,a[i-1]);
}
for(register int i=1;i<=n;i++)
pq.push(Node(1,i,i));
while(k--)
{
Node u=pq.top();
pq.pop();
ans+=u.val;
if(u.l<u.id)
pq.push(Node(u.l,u.id-1,u.x));
if(u.id<u.r)
pq.push(Node(u.id+1,u.r,u.x));
}
printf("%lld\n",ans);
return 0;
}
三.KMP算法:
1.解决的问题:
在一个字典串中查询一个查询串的位置。
2.实现:
我们可以直接暴力匹配,但显然,时间复杂度会炸,于是就考虑 \(\text{KMP}\)。
我们用如下方法匹配:
我们找到一个前缀与一个后缀,即图中的阴影部分。
且我们的工作原理就是 \(i\) 指针不动,\(j\) 指针向前跳,这样才能实现将查询串向后拖的效果。
关键点:
- 一旦字符串失配,我们希望查询串能尽可能多的跳跃,而不是逐位移动;
- 能够跳跃的最大距离由查询串的最长公共前后缀的长度决定。
对于上面的例子:
- 失配前字串 \(\text{x y x y x y}\)
- 前缀集合 \(\text{{x,xy,xyx,xyxy,xyxyx}}\)
- 后缀集合 \(\text{{yxyxy,xyxy,yxy,xy,y}}\)
- 最长公共前后缀 \(\text{xyxy}\)
然后我们可以用个 \(\text{PMT(Partial Match Table)}\) 表来理解一下:
那么如何求 \(\text{nxt}\) 数组呢?
- 求 \(\text{nxt}\) 数组也是字符串匹配的过程;
- 将目标串查询串当作主串,并把查询串自己当作目标串,自己匹配自己;
- \(i\) 应该从 \(1\) 开始。
下图展示了其工作过程:
\(\text{getnxt}\) 函数:
int nxt[MAXN];
int len1,len2;
inline void getnxt(string s)
{
nxt[0]=-1;
int i=0,j=-1;
while(i<len2)
{
if(j==-1 || s2[i]==s2[j])
i++,j++,nxt[i]=j;
else
j=nxt[j];
}
return;
}
\(\text{KMP}\) 函数:
inline void KMP(string s1,string s2)
{
getnxt(s2);
int i=0,j=0;
while(i<len1)
{
if(j==len2-1 && s1[i]==s2[j])
{
printf("%d\n",i-j+1);
j=nxt[j];
}
if(j==-1 || s1[i]==s2[j])
i++,j++;
else
j=nxt[j];
}
return;
}
典型例题
例一 P3375 【模板】KMP字符串匹配
模板题。。。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int nxt[MAXN];
int len1,len2;
string s1,s2;
inline void getnxt(string s2)
{
nxt[0]=-1;
int i=0,j=-1;
while(i<len2)
{
if(j==-1 || s2[i]==s2[j])
i++,j++,nxt[i]=j;
else
j=nxt[j];
}
return;
}
inline void KMP(string s1,string s2)
{
getnxt(s2);
int i=0,j=0;
while(i<len1)
{
if(j==len2-1 && s1[i]==s2[j])
{
printf("%d\n",i-j+1);
j=nxt[j];
}
if(j==-1 || s1[i]==s2[j])
i++,j++;
else
j=nxt[j];
}
return;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>s1>>s2;
len1=s1.size(),len2=s2.size();
KMP(s1,s2);
for(register int i=1;i<=len2;i++)
printf("%d ",nxt[i]);
return 0;
}
例二 P4391
结论题。。。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5;
int nxt[MAXN];
int n;
int len;
inline void getnxt(string s)
{
nxt[0]=-1;
int i=0,j=-1;
while(i<n)
{
if(j==-1 || s[i]==s[j])
i++,j++,nxt[i]=j;
else
j=nxt[j];
}
return;
}
string s;
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
cin>>s;
getnxt(s);
printf("%d\n",n-nxt[n]);
return 0;
}
例三 P1470 最长前缀 Longest Prefix
思路:
1.将给出的串当作字典串。
2.将集合中的串当作查询串做 \(\text{KMP}\) 匹配。
3.使用差分数组,标记第一个查询串出现的位置。
4.维护前缀和,从 \(1\) 开始枚举,第一次没有标记的位置 \(\text{break}\),输出答案。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5;
const int N=300;
int nxt[MAXN];
string B[N],s;
int cnt;
int dif[MAXN];
string s1;
inline void getnxt(string s2)
{
nxt[0]=-1;
int i=0,j=-1;
int len2=s2.size();
while(i<len2)
{
if(j==-1 || s2[i]==s2[j])
i++,j++,nxt[i]=j;
else
j=nxt[j];
}
return;
}
inline void KMP(string s1,string s2)
{
getnxt(s2);
int i=0,j=0;
int len1=s1.size(),len2=s2.size();
while(i<len1)
{
if(j==len2-1 && s1[i]==s2[j])
{
int st=i-j+1;
dif[st]+=1;
dif[i+2]-=1;
j=nxt[j];
}
if(j==-1 || s1[i]==s2[j])
i++,j++;
else
j=nxt[j];
}
return;
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
while(cin>>s1)
{
if(s1==".")
break;
cnt++;
B[cnt]=s1;
}
while(cin>>s1)
s=s+s1;
for(register int i=1;i<=cnt;i++)
KMP(s,B[i]);
int len=s.size();
for(register int i=1;i<=len;i++)
dif[i]+=dif[i-1];
for(register int i=1;i<=len;i++)
if(dif[i]==0)
{
printf("%d\n",i-1);
return 0;
}
cout<<s.size();
return 0;
}
例四 CF1200E Compress Words
这道题基本的思路很容易想到,我们可以将当前需要拼接的串先放到已拼接好的串前面,那么不难发现,要求的就是这玩意儿的最长公共前后缀,那么这就是一个 \(\text{KMP}\) 求 \(\text{nxt}\) 数组的模板。
但很显然,这样做的时间复杂度为 \(O(n·len)\),\(10^5 \times 10^6\) 会炸,所以我们考虑到,求 \(\text{nxt}\) 数组时,若串特别长,且最长公共前后缀特别短时,那么就会做很多无用的操作,就会导致 \(\text{TLE}\),那么我们可以考虑只截取开头和结尾的一小部分,就可以解决时间复杂度超的问题。
这里还要注意一个点,就是在拼接时,带拼接的串可能比已拼接的串还要长,那么我们就需要从两串之间取更小的串截取。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int tot,len,maxn,minn;
int ans,nxt[MAXN];
char s[MAXN],t[MAXN*5];
inline void getnxt(char x[])
{
tot=len=strlen(x+1);
minn=min(len,maxn);
x[++tot]='#';
for(register int i=1;i<=minn;i++)
x[++tot]=t[maxn-(minn-i)];
nxt[1]=nxt[0]=0;
for(register int i=1,j=0;i<tot;i++)
{
j=nxt[i];
while(j && x[i+1]!=x[j+1])
j=nxt[j];
if(x[i+1]==x[j+1])
j++;
nxt[i+1]=j;
}
for(register int i=nxt[tot]+1;i<=len;i++)
t[++maxn]=x[i];
return;
}
int n;
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(register int i=1;i<=n;i++)
{
cin>>s+1;
getnxt(s);
}
for(register int i=1;i<=maxn;i++)
cout<<t[i];
return 0;
}
例五 P3435 OKR-Periods of Words
不同于其他题的是,这道题要我们找的是最短公共前后缀,那么我们就只需要将最长的求出,然后不停的j=nxt[j]
的跳就行了。
Code
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e6+5;
int n,cnt;
int nxt[MAXN];
char a[MAXN];
inline void getnxt()
{
nxt[1]=0;
int j=0;
for(register int i=2;i<=n;i++)
{
while(j>0 && a[i]!=a[j+1])
j=nxt[j];
if(a[i]==a[j+1])
j++;
nxt[i]=j;
}
return;
}
inline void KMP()
{
int j=0;
for(register int i=1;i<=n;i++)
{
j=i;
while(nxt[j])
j=nxt[j];
if(nxt[i]!=0)
nxt[i]=j;
cnt+=i-j;
}
return;
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
cin>>a+1;
getnxt();
KMP();
printf("%lld\n",cnt);
return 0;
}