字符串学习笔记

哈希


引入:给定两个字符串 \(S\)\(T\),比较他们是否本质相同。

直接比较是 \(O(n)\) 的,显然不优。为此我们引入哈希的概念:我们给每个字符串赋予一个权值,比较权值的大小即可。

那么一个字符串的权值如何得到?我们钦定基数 \(base\) 与模数 \(mod\),其中 \(base\) 为大于字符种数的较小质数,\(mod\) 为大质数(例如 \(998244353\)\(1000000007\)),则权值通过以下方法得到:

\[ans=(\sum\limits_{i=1}^{|S|}\ S_{i} \times base^{i-1}) \% mod \]

显然会有一定概率错,但是正确率极高。

KMP 算法


定义:若 \(S\) 的一个前缀同时是其后缀,则这个前缀称作 \(S\) 的一个 border。

定义:称 \(S\) 有长度为 \(p\) 的周期,满足 \(S_{i}=S_{i+p}(1 \le i \le n-p)\)

性质:

  1. \(S_{[1,p]}\)\(S\) 的 border,则其周期为 \(|S|-p\)

  2. \(S\) 的 border 的 border 也是 \(S\) 的 border。

kmp 用于字符串匹配相关问题,先抛一个最基本的问题:给定文本串 \(S\) 和模式串 \(T\),问在 \(S\) 中那些位置能匹配到模式串 \(T\)

定义:\(\mathrm{nxt_{i}}\) 表示模式串的长度为 \(i\) 的前缀的最长 border 的长度。首先是一个个匹配,若在位置 \(j\) 遇到匹配不到的,立刻跳回上一个 \(\mathrm{nxt_{j}}\),由于前缀 border 和后缀 border 相同,后面匹配过了,所以前面的也一定可以匹配,所以直接跳回最长 border 一定最优。

放个图:

假设 \([1,j-1\)] 部分已经匹配到,而第 \(j\) 个位置不匹配,前 \(j-1\) 的最长 border 长度为 \(i\),那么下一次我不需要重新从 \(1\) 开始匹配。图中可以发现由于前 \(j-1\) 已经匹配,那么 \(T\) 的第一段和 \(S\) 的最后一段也一定相同(四块阴影都相同)。所以失配后跳到这样:

即跳到 \(k-1\) 这个位置,前面的都相同,继续匹配看第 \(k\) 个位置能否匹配,若还是失配则继续跳最长 border,跳最长的能保证在正确的前提下最大化减少时间复杂度,这里最长 border 的位置即为 \(\mathrm{nxt}\) 数组。求 \(\mathrm{nxt}\) 数组的过程可以感性理解为字符串 \(T\) 自己跟自己匹配,多画图吧。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int n,m,i,j,ans,a,b,c,nxt[N];
char s[N],t[N];
int main(){
	scanf("%s",s+1);
	scanf("%s",t+1);
	n=strlen(s+1),m=strlen(t+1);
	for(i=2;i<=m;i++){
		while(j && t[j+1]!=t[i]) j=nxt[j];
		if(t[j+1]==t[i]) j++;
		nxt[i]=j;
	}
	j=0;
	for(i=1;i<=n;i++){
		while(j && t[j+1]!=s[i]) j=nxt[j];
		if(t[j+1]==s[i]) j++;
		if(j==m){
			printf("%d\n",i-j+1);
			j=nxt[j];
		}
	}
	for(i=1;i<=m;i++) printf("%d ",nxt[i]);
	return 0;
}

练习:CF1200E Compress Words

很显然把下一个串当前缀,把当前串当后缀不断跑 kmp 即可,但是这样会超时,取 \(\min(|S|,|T|)\) 当前后缀即可。注意这里的 \(S\) 指的是 \(T\) 前面所有的串组成的答案。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int n,m,i,j,ans,nxt[N],T,len,k,r,lst,cnt,fir,Min,x;
char s[N],t[N],pr[N],p[N];
int main(){
	scanf("%d",&T);
	if(T==1){
		scanf("%s",s+1);
		printf("%s",s+1);
		return 0;
	}
	T--,scanf("%s",s+1),n=strlen(s+1);
	while(T--){
		scanf("%s",t+1),k=0,len=strlen(t+1);
		cnt++,Min=min(n,len);
		for(i=1;i<=Min;i++) p[++k]=t[i];
		for(i=n-Min+1;i<=n;i++) p[++k]=s[i];
		j=0,nxt[1]=0;
		for(i=2;i<=k;i++){
			while(j && p[j+1]!=p[i]) j=nxt[j];
			if(p[j+1]==p[i]) j++;
			nxt[i]=j;
		}
		x=nxt[k];
		while(x>Min) x=nxt[x];
		for(i=x+1;i<=len;i++) s[++n]=t[i];
	}
	for(i=1;i<=n;i++) printf("%c",s[i]);
	return 0;
}

fail 失配树


若我们要求某个前缀的 border 个数怎么求?不断跳 \(\mathrm{nxt}\) 自然是可行的,我们也可以考虑建出失配树。

对于 \(S\) 的每个前缀 \([1,i]\),我们连一条 \((\mathrm{nxt_{i}},i)\) 的有向边,那么最终有了一棵 \(n+1\) 个节点的以 \(0\) 为根的有向树。性质:

  1. 对于一个点 \(i\),它的 border 集合便是它到树根的路径上的所有点对应的前缀。

  2. \(u\) 和点 \(v\) 在 fail 树上的 lca 指的是长度分别为 \(u\)\(v\) 的前缀的最长公共 border。

那么前面那个问题的答案实际就是节点的深度值。

模板题:P5829 【模板】失配树

就是求两个前缀的最长公共 border,那么找出他们在 fail 树上的 lca 即可,复杂度 \(O(n \log n)\)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2000005
int n,m,i,j,ans,a,b,c,k,t,p,q,nxt[N],dep[N],f[N][25],LG[N];
char s[N];
vector<int>G[N];
void dfs(int k,int lst){
	dep[k]=dep[lst]+1,f[k][0]=lst;
	for(int i=1;i<=LG[dep[k]];i++) f[k][i]=f[f[k][i-1]][i-1];
	for(auto y:G[k]) dfs(y,k);
}
int lca(int a,int b){
	if(dep[b]>dep[a]) swap(a,b);
	for(int i=20;i>=0;i--){
		if(dep[f[a][i]]>=dep[b]) a=f[a][i];
	}
	if(a==b) return f[a][0];
	for(int i=20;i>=0;i--){
		if(f[a][i]!=f[b][i]) a=f[a][i],b=f[b][i];
	}
	return f[a][0];
}
int main(){
	scanf("%s",s+1),n=strlen(s+1);
	LG[1]=0;for(i=2;i<=n;i++) LG[i]=LG[i-1]+((i&(-i))==i); 
	j=0,nxt[1]=0;
	for(i=2;i<=n;i++){
		while(j && s[j+1]!=s[i]) j=nxt[j];
		if(s[j+1]==s[i]) j++;
		nxt[i]=j;
	}
	for(i=1;i<=n;i++) G[nxt[i]].push_back(i);
	dep[n+1]=-1;dfs(0,n+1);
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&p,&q);
		printf("%d\n",lca(p,q));
	} 
	return 0;
}

练习:P2375 [NOI2014] 动物园

  1. 这个题首先可以直接 kmp,若跑的过程中前缀长度 \(k\) 满足 \((k+1) \times 2>i\) 则及时跳 \(\mathrm{nxt}\) 即可,正确性证明有点难,这里不再赘述,复杂度 \(O(|S|+|T|)\)。代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
#define int long long
const int mod=1e9+7;
int n,m,i,j,ans,nxt[N],t,k,pr[N],sum,G[N],W[N],num[N];
char s[N];
signed main(){
	scanf("%lld",&t);
	while(t--){
		scanf("%s",s+1),n=strlen(s+1),ans=1;
		memset(nxt,0,sizeof(nxt));
		memset(num,0,sizeof(num));
		j=0,nxt[1]=0,num[1]=1;
		for(i=2;i<=n;i++){
			while(j && s[j+1]!=s[i]) j=nxt[j];
			while(k && (s[k+1]!=s[i] || (k+1)*2>i)) k=nxt[k];
			if(s[j+1]==s[i]) j++;
			if(s[k+1]==s[i]) k++;
			nxt[i]=j,num[i]=num[nxt[i]]+1;
			ans=(ans*(num[k]+1))%mod;
		}
		if(t!=0) printf("%lld\n",ans);
		else printf("%lld",ans);
	}
	return 0;
}
  1. 稍微无脑的做法我们可以考虑建立失配树,对于每个节点,我们只需在失配树上用倍增找出最大的 border 长度满足 \(2 \times k \le i\),记为节点 \(p\),那么答案乘上 \(dep_{p}+1\) 即可,复杂度 \(O(|S| \log |S|)\),跑步过去,但是只需要玄学把倍增数组两个维度反过来即可 AC。代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2000005
#define int long long
const int mod=1e9+7;
int n,m,i,j,ans,a,b,c,k,t,p,q,nxt[N],dep[N],f[25][N],LG[N],u[N],h[N];
char s[N];
struct AB{
	int a,b,n;
}d[N];
void cun(int a,int b){
	d[++k].a=a,d[k].b=b;
	d[k].n=h[a],h[a]=k;
}
queue<int>Q,L;
void bfs(int k){
	Q.push(k),L.push(n+1);
	while(!Q.empty()){
		a=Q.front(),Q.pop(),b=L.front(),L.pop(),f[0][a]=b;
		for(int i=1;i<=LG[dep[a]];i++) f[i][a]=f[i-1][f[i-1][a]];
		for(int i=h[a];i;i=d[i].n){
			int b=d[i].b;
			dep[b]=dep[a]+1;
			Q.push(b),L.push(a);
		}
	}
}
int get(int Y,int x){
	for(int i=LG[dep[x]];i>=0;i--){
		if(f[i][x]*2>Y) x=f[i][x];
	}
	return dep[f[0][x]];
}
signed main(){
	scanf("%lld",&t);
	while(t--){
		for(i=0;i<=n+1;i++) h[i]=0;
		for(i=0;i<=n+1;i++){
			for(j=0;j<=20;j++) f[j][i]=0;
		}
		scanf("%s",s+1),n=strlen(s+1),ans=1,k=0;
		LG[1]=0;for(i=2;i<=n;i++) LG[i]=LG[i-1]+((i&(-i))==i); 
		j=0,nxt[1]=0;
		for(i=2;i<=n;i++){
			while(j && s[j+1]!=s[i]) j=nxt[j];
			if(s[j+1]==s[i]) j++;
			nxt[i]=j;
		}
		for(i=1;i<=n;i++) cun(nxt[i],i);
		cun(n+1,0);
		dep[n+1]=-1,bfs(n+1);
		for(i=1;i<=n;i++){
			int p=get(i,i);
			ans=(ans*(p+1))%mod; 
		}
		if(t!=0) printf("%lld\n",ans);
		else printf("%lld",ans);
	}
	
	return 0;
}

AC 自动机


posted @ 2023-08-01 10:30  Nwayy  阅读(39)  评论(0)    收藏  举报
/* 鼠标点击求赞文字特效 */ /*鼠标跟随效果*/