字符串

复健Day9

字符串相关算法

1.最小表示法

最小表示法就是找出字符串s的循环同构串中字典序最小的那一个

时间复杂度为O(n)

char s[maxn];
int n;

int get_min(char *s)
{
	n=strlen(s+1);
	for(int i=1;i<=n;i++) s[n+i]=s[i];
	int i=1,j=2,k=0;
	while(i<=n&&j<=n)
	{
		for(k=0;k<n&&s[i+k]==s[j+k];k++);
		s[i+k]>s[j+k]?i=i+k+1:j=j+k+1;
		if(i==j) j++;//若跳转后两个指针相同,则j++,保证比较的两个字符串不同
	}
	return min(i,j);
}

模板

https://www.luogu.com.cn/problem/P1368

#include<iostream>
#include<cstdio>
#include<algorithm>
#define maxn 600010
using namespace std;

int s[maxn];
int n;

int get_min()
{
	for(int i=1;i<=n;i++) s[i+n]=s[i];
	int i=1,j=2,k=0;
	while(i<=n&&j<=n)
	{
		for(k=0;k<n&&s[i+k]==s[j+k];k++);
		s[i+k]>s[j+k]?i=i+k+1:j=j+k+1;
		if(i==j) j++;
	}
	return min(i,j);
}

int main()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>s[i];
	int start=get_min();
	for(int i=start;i<=start+n-1;i++) printf("%d ",s[i]);
	return 0;
}

2.字符串哈希

把不同的字符串映射成不同的整数

对于一个长度为n的字符串s,这样定义Hash函数:h(s)=i=1ns[i]×pni(modM)

abc哈希值为a×p2+b×p+c

其中,p为一个质数,通常取其为131或者13331M通常取大整数264,把哈希函数值h定义为NULL,超过则自动溢出,等价于取模

typedef unsigned long long ULL
const int P=131;

ULL p[maxn],h[maxn];

void init()//预处理hash函数的前缀和
{
	p[0]=1,h[0]=0;
	for(int i=1;i<=n;i++)
	{
		p[i]=p[i-1]*P;;
		h[i]=h[i-1]*P+s[i];
	}
}

ULL get(int l,int r)//计算 s[l~r]的hash值
{
	return h[r]-h[l-1]*p[r-l+1];
}

bool substr(int l1,int r1,int l2,int r2)//判断两子串是否相等
{
	return get(l1,r1)==get(l2,r2);
}

模板

https://www.luogu.com.cn/problem/P3370

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<set>
#define maxn 10010
#define N 1500
#define ULL unsigned long long
using namespace std;

const int P=131;

ULL p[maxn],h[maxn];
set<ULL> S;

void init(char *s)
{
	p[0]=1,h[0]=0;
	for(int i=1;i<=N;i++)
	{
		p[i]=p[i-1]*P;
		h[i]=h[i-1]*P+s[i];
	}
}

ULL get(int l,int r)
{
	return h[r]-h[l-1]*p[r-l+1];
}

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		char s[N];
		cin>>s;
		init(s+1);
		int len=strlen(s+1);
		S.insert(get(1,len));
	}
	printf("%d\n",S.size());
	return 0;
}

3.KMP算法

给定一个模式串P和一个主串S,求模式串P再主串S中出现的位置(字符串下标均从1开始)

next[i]表示模式串P[1,i]中相等前后缀的最长长度

使用双指针计算ne数组

ne[1]=0;
for(int i=2,=0;i<=n;i++)
{
	while(j&&P[i]!=P[j+1]) j=ne[j];//若P[i]!=P[j+1],让j回跳到能匹配的位置
	if(P[i]==P[j+1]) j++;//指向匹配前缀的末尾
	ne[i]=j;
}

模式串与主串匹配

for(int i=1,j=0;i<=m;i++)
{
	while(j&&S[i]!=P[j+1]) j=ne[j];
	if(S[i]==P[j+1]) j++;
	if(j==n) printf("%d\n",i-n+1);//输出匹配开始的位置
}

模板

https://www.luogu.com.cn/problem/P3375

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 1000010
using namespace std;

int ne[maxn];
char S[maxn],P[maxn];

int main()
{
	cin>>(S+1);
	cin>>(P+1);
	int m=strlen(S+1);
	int n=strlen(P+1);
	ne[1]=0;
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&P[i]!=P[j+1]) j=ne[j];
		if(P[i]==P[j+1]) j++;
		ne[i]=j;
	}
	for(int i=1,j=0;i<=m;i++)
	{
		while(j&&S[i]!=P[j+1]) j=ne[j];
		if(S[i]==P[j+1]) j++;
		if(j==n) printf("%d\n",i-n+1);
	}
	for(int i=1;i<=n;i++) printf("%d ",ne[i]);
	return 0;
}

4.扩展KMP算法(Z函数)

Z函数:对于一个长度为n 的字符串Sz[i]表示S与其后缀S[i,n]的最长公共前缀(LCP)的长度

Zbox:对于i,我们称区间[i,i+z[i]1]i的匹配串,也可以叫Zbox

void get_z(char *s,int n)//s与s的每一个后缀的LCP的长度数组z
{
	z[1]=n;
	for(int i=2,l,r=0;i<=n;i++)//维护盒子s[l~r],s[l~r]=s[1~r-l+1]
	{
		if(i<=r) z[i]=min(z[i-l+1],r-i+1);//若i在盒子内,则s[i~r]=s[i-l+1~r-l+1]
		while(s[1+z[i]]==s[i+z[i]]) z[i]++;//这是(z[i-l+1]>=r-i+1之后)或者(i>r即在盒外)的暴力枚举
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;//求出z[i]后,如果i+z[i]-1>r,则更新盒子l=i,r=i+z[i]-1
	}
}
void get_p(char *s,int n,char *t,int m)//s与t的每一个后缀的LCP的长度数组
{
	for(int i=1,l,r=0;i<=m;i++)
	{
		if(i<=r) p[i]=min(z[i-l+1],r-i+1);
		while(1+p[i]<=n&&i+p[i]<=m&&s[1+p[i]]==t[i+p[i]]) p[i]++;
		if(i+p[i]-1>r) l=i,r=i+p[i]-1;
	}
}

模板

https://www.luogu.com.cn/problem/P5410

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int maxn=2e7+5;

int z[maxn],p[maxn];
char s[maxn],t[maxn];

long long get_ans(int *s,int n)
{
	long long ans=0;
	for(int i=1;i<=n;i++)
	{
		ans^=1LL*i*(s[i]+1);
	}
	return ans;
}

void get_z(char *s,int n)
{
	z[1]=n;
	for(int i=2,l,r=0;i<=n;i++)
	{
		if(i<=r) z[i]=min(z[i-l+1],r-i+1);
		while(s[1+z[i]]==s[i+z[i]]) z[i]++;
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
}

void get_p(char *s,int n,char *t,int m)
{
	for(int i=1,l,r=0;i<=m;i++)
	{
		if(i<=r) p[i]=min(z[i-l+1],r-i+1);
		while(1+p[i]<=n&&i+p[i]<=m&&s[1+p[i]]==t[i+p[i]]) p[i]++;
		if(i+p[i]-1>r) l=i,r=i+p[i]-1;
	}
}

int main()
{
	cin>>(t+1);
	cin>>(s+1);
	int n=strlen(s+1);
	int m=strlen(t+1);
	get_z(s,n);//注意这里是s
	get_p(s,n,t,m);//注意这里是s,t
	printf("%lld\n",get_ans(z,n));
	printf("%lld\n",get_ans(p,m));
}

5.Manacher(马拉车)

可以在O(n)时间内求出一个字符串中的最长回文串

首先要改造字符串:在字符之间和串两端插入#,改造后,都变成奇回文串

scanf("%s",a+1);
int n=strlen(a+1),k=0;
s[0]='$',s[++k]='#';//s[0]='$',是哨兵(边界)
for(int i=1;i<=n;i++) s[++k]=a[i],s[++k]='#';
n=k;

回文半径d[i]:以i为中心的最长回文串的长度的一半

加速盒子[l,r](与扩展KMP算法中的盒子是一样的):算法过程中我们要维护右端点最靠右的最长回文串,而利用盒子就可以借助之前的状态来加速计算新的状态

void get_d(char *s,int n)
{
	d[1]=1;
	for(int i=2,l,r=0;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;
	}
}

原串的最长回文串就是新船的最大半径1

模板

https://www.luogu.com.cn/problem/P3805

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int maxn=3e7;

char a[maxn],s[maxn];
int d[maxn];

void get_d(char *s,int n)
{
	d[1]=1;
	for(int i=2,l,r=0;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;
	}
}

int main()
{
	cin>>(a+1);
	int n=strlen(a+1);
	int k=0;
	s[0]='$',s[++k]='#';
	for(int i=1;i<=n;i++) s[++k]=a[i],s[++k]='#';
	n=k;
	get_d(s,n);
	int ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,d[i]);
	printf("%d\n",ans-1);
	return 0;
}

6.字典树(Trie)

Trie维护字符串的集合,支持两种操作:插入和查询

ch[p][i]表示节点p沿着边i(i是字母映射后的值)走到的节点编号

在单词结束点记录插入次数

void insert(char *s)
{
	int p=0;
	for(int i=0;s[i];i++)
	{
		int j=s[i]-'a';
		if(!ch[p][i]) ch[p][i]=++idx;//如果没有子结点,创建子结点
		p=ch[p][j];
	}
	cnt[p]++;//插入次数
}
int query(char *s)
{
	int p=0;
	for(int i=0;s[i];i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j]) return 0;
		p=ch[p][j];
	}
	return cnt[p];
}

7.最大异或对(01Trie)

给定N个整数a1,a2,,an,任选两个数进行异或运算,求得到的结果最大是多少

N个整数用二进制表示,所以我们的Trie数位一个二叉树,深度为31

const int maxn=100010;
int n,a[maxn];
int ch[maxn*31][2],idx;

void insert(int x)
{
	int p=0;
	for(int i=30;i>=0;i--)
	{
		int j=x>>i&1;
		if(!ch[p][j]) ch[p][j]=++idx;
		p=ch[p][j];
	}
}

int query(int x)
{
	int p=0,res=0;
	for(int i=30;i>=0;i--)
	{
		int j=x>>i&1;//取出第i位
		if(ch[p][!j])//可以找到与之相反的数
		{
			res+=1<<i;
			p=ch[p][!j];
		}
		else p=ch[p][j];
	}
	return res;
}

8.AC自动机

是一个多模式匹配算法:给定n个模式串和一个主串,查找有多少个模式串在主串中出现过

1.首先,我们构造Trie树:用一个节点表示一个从根到当前节点的字符串,例如:节点5表示字符串she

如果节点是个模式串,则需打个标记cnt[i]=1

2.然后,我们构造AC自动机:在Trie上构建两类边:回跳边转移边

ne[v]存节点v的回跳边的终点

回跳边指向父节点的回跳边所指节点的儿子(v,u,ne[v],ne[u])构成四边形

回跳边所指节点一定是当前节点的最长后缀(如果这里失配,那么我们转移到回跳边去继续匹配)

ch[u][i]存节点u的树边的终点,也存节点u的转移边的终点

转移边指向当前节点的回跳边所指节点的儿子(u,ne[u],ch[][])构成三角形

转移边所指节点一定是当前节点的最短路(就是说从当前节点到i的最短路,不必再回到前面的祖先节点去,节约了时间)

void build()//建AC自动机
{
	queue<int>	q;
	for(int i=0;i<26;i++)
		if(ch[0][i]) q.push(ch[0][i]);
	while(q.size())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=ch[u][i];
			if(v) ne[v]=ch[ne[u]][i],q.push(v);
			else ch[u][i]=ch[ne[u]][i];//若儿子不存在,则父节点自建转移边
		}
	}
}

3.扫描主串匹配

int query(char *s)
{
	int ans=0;
	for(int k=0,i=0;s[k];k++)
	{
		i=ch[i][s[k]-'a'];//沿树边或者转移边走,保证不回退
		for(int j=i;j&&~cnt[j];j=ne[j])//沿回跳边搜索模式串
		{
			ans+=cnt[j],cnt[j]=-1;//这样的话,就是只累计不重复的可匹配的字符串个数,如果出现一次就累计一次,就去掉									  //cnt=-1和循环里的~cnt的条件
		}
	}
	return ans;
}

模板

https://www.luogu.com.cn/problem/P3808

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#define maxn 1000010
using namespace std;

int ch[maxn][26],cnt[maxn],idx;
int ne[maxn];
char s[maxn];

void insert(char *s)
{
	int p=0;
	for(int i=0;s[i];i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j]) ch[p][j]=++idx;
		p=ch[p][j];
	}
	cnt[p]++;
}

void build()
{
	queue<int> q;
	for(int i=0;i<26;i++) if(ch[0][i]) q.push(ch[0][i]);
	while(q.size())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=ch[u][i];
			if(v) ne[v]=ch[ne[u]][i],q.push(v);
			else ch[u][i]=ch[ne[u]][i];
		}
	}
}
int query(char *s)
{
	int ans=0;
	for(int k=0,i=0;s[k];k++)
	{
		i=ch[i][s[k]-'a'];
		for(int j=i;j&&~cnt[j];j=ne[j])
		{
			ans+=cnt[j];
			cnt[j]=-1;
		}
	}
	return ans;
}

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>s;
		insert(s);
	}
	build();
	cin>>s;
	printf("%d\n",query(s));
	return 0;
}

之后有空再学习:

9.后缀自动机(SAM)

有链接边和转移边

ch[x][c]存节点x的转移边的终点,fa[x]存节点x的链接边的终点,len[x]存节点x的最长串的长度


10.后缀数组(SA)

sa[i]表示排名为i的后缀编号,rk[i]表示后缀i的排名,height[i]=lap(sa[i],sa[i1])表示第i名后缀与第i1名后缀的最长公共前缀的长度(高度数组表示两个后缀的相似度,排序相邻的两个后缀相似度最高)

void get_sa()//n为后缀个数,m为桶的个数,桶数组x,辅助数组y,计数数组c
{
	int i,j,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;i--) sa[c[x[i]]--]=i;//后缀i的排序是i所在的桶号的剩余累计值
	for(k=1;k<=n;k++)
	{
		//按第二关键字排序
		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;i--) sa[c[x[y[i]+k]]--]=y[i];//后缀y[i]的排序是第二关键字所在的桶号的剩余累计值
	}
}

posted on   dolires  阅读(11)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示