各类字符串算法的部分细节

前言

本文主要说KMP算法、字符串哈希(BKDR)、Manacher算法、Trie树、AC自动机、扩展KMP(Z函数)、后缀数组(SA)和后缀自动机(SAM)的一些细节。
关于回文自动机(PAM),等到我再学习一下。

KMP算法

#include<iostream>
using namespace std;
int main() {
	string s1,s2;
	cin>>s1>>s2;
	int nxt[s2.size()];
	for(auto&i:nxt) i=0;
	for(int i=1,j=0;i<s2.size();i++) {
		while(j&&s2[i]^s2[j]) 
			j=nxt[j-1];
		if(s2[i]==s2[j]) j++;
		nxt[i]=j;
	}
	for(int i=0,j=0;i<s1.size();i++) {
		while(j&&s1[i]^s2[j])
			j=nxt[j-1]; 
		if(s1[i]==s2[j]) j++;
		if(j==s2.size()) {
			cout<<i-j+2<<endl;
			j=nxt[j-1];
		}
	} 
	for(auto&i:nxt)
		cout<<i<<' ';
	return 0;
}
  1. 所有跳转都是j=nxt[j-1] (如果从0开始)
  2. nxt数组初值为0

字符串哈希

#include<iostream>
using namespace std;
int n;
bool t[100000000];
long long p[1505];
long long m=99999989;
int cnt;
int main() {
	cin>>n;
	p[0]=1;
	for(int i=1;i<=1500;i++)
		p[i]=(p[i-1]*13331)%m;
	while(n--) {
		string s;
		cin>>s;
		long long h=0;
		for(int i=0;i<s.size();i++)
			h=(h+s[i]*p[s.size()-i-1])%m;
		if(!t[h]) cnt++,t[h]=true;
	}
	cout<<cnt;
	return 0;
}

这里选择的质数是13331,模数是99999989。
然后是关于哈希冲突的概率分析:

绝大多数情况下,不要选择一个19级别的数,因为这样随机数据都会有Hash冲突,根据生日悖论,随便找上109个串就有大概率出现至少一对Hash 值相等的串(参见BZOJ 3098 Hash Killer II)。
最稳妥的办法是两个19级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)(参见BZOJ 3099 Hash Killer III)。
如果能背过或在考场上找出一个1018级别的质数(Miller-Rabin),也相对靠谱,主要用于前一种担心会超时,后一种担心被卡。
偷懒的写法就是直接使用unsigned long long,不手动进行取模,它溢出时会自动进行取模,如果出题人比较良心,这种做法也不会被卡,但这个是完全可以卡的,卡的方法参见BZOJ 3097 Hash Killer I。

所以说这里的模数是很小的,很险。

Manacher算法

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
char s[11000002];
char a[11000002<<1];
int h[11000002<<1];
int n;
int main() {
	cin>>s+1;
	n=strlen(s+1);
	a[0]='$';
	a[1]='#';
	for(int i=1,t=2;i<=n;i++) 
		a[t]=s[i],a[t+1]='#',t+=2;
	n=(n<<1)+2;
	a[n]='*';
	int l=0,r=0;
	for(int i=1;i<=n;i++) {
		if(i<=r) h[i]=min(h[l+r-i],r-i+1);
		while(a[i+h[i]]==a[i-h[i]]) h[i]++;
		if(i+h[i]>r) r=i+h[i]-1,l=i-h[i]+1;
	}
	cout<<*max_element(h+1,h+1+n)-1;
	return 0;
}
  1. 其实a[n]的地方并不需要设为’*‘,因为默认就是’\0’
  2. 注意更新右端点的条件。

Trie树

Trie树还可以解决最长异或路径的问题。

AC自动机

#include<iostream>
#include<queue>
using namespace std;
int t[1000005][26],top,h[1000005],nxt[1000005];
int n;
char f(char x) {
	return x-'a';
}
void push(string&s) {
	int x=0;
	for(int i:s) {
		i=f(i);
		if(t[x][i]) x=t[x][i];
		else {
			t[x][i]=++top;
			x=t[x][i];
		}
	}
	h[x]++;
}
void bfs() {
	queue<int> q;
	for(int i=0;i<26;i++)
		if(t[0][i]) 
			q.push(t[0][i]);
	while(!q.empty()) {
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++) {
			int v=t[u][i];
			if(v) nxt[v]=t[nxt[u]][i],q.push(v);
			else t[u][i]=t[nxt[u]][i];
		}
	}
}
int solve(string&s) {
	int ans=0;
	int i=0;
	for(auto&x:s) {
		i=t[i][f(x)];
		for(int j=i;j&&~h[j];j=nxt[j])//h[j]被访问过了,那么h[j]接下来的节点肯定也被访问过了 
			ans+=h[j],h[j]=-1;
	}
	return ans;
}
int main() {
	cin>>n;
	for(int i=1;i<=n;i++) {
		string s;
		cin>>s;
		push(s);
	}
	bfs();
	string s;
	cin>>s;
	cout<<solve(s);
	return 0;
}

代码很简单,没什么细节。

后缀数组(SA)

后缀数组主要讨论后缀数组(sa)、名次数组(rk)和高度数组(h)。获得sa数组后,通过如下性质能够很容易地O(n)求出高度数组:
h[rk[i]]>=h[rk[i-1]]-1
性质很显然。
sa数组中相邻的两个元素最为相似。
通过快速排序可以O(nlog2n)求出sa数组。通过桶排序和倍增法可以O(nlogn)求出。当然还有一些复杂方法可以O(n)求出。

后缀排序代码如下:

#include<iostream>
#include<cstring>
using namespace std;
char s[2000005];
typedef int intx[2000005];
intx f,sa,x;
int t[2000005];
//f数组
int n,m=127;
void SA() {
	n=strlen(s+1);
	for(int i=1; i<=n; i++) t[f[i]=s[i]]++;
	for(int i=1; i<=m; i++) t[i]+=t[i-1];
	for(int i=n; i; i--) sa[t[f[i]]--]=i;
	for(int k=1; k<=n; k<<=1) {
		memset(t,0,sizeof t);
		for(int i=1; i<=n; i++) x[i]=sa[i];
		for(int i=1; i<=n; i++) t[f[i+k]]++;//
		for(int i=1; i<=m; i++) t[i]+=t[i-1];
		for(int i=n; i; i--) sa[t[f[x[i]+k]]--]=x[i];
		memset(t,0,sizeof t);
		for(int i=1; i<=n; i++) x[i]=sa[i];
		for(int i=1; i<=n; i++) t[f[i]]++;//
		for(int i=1; i<=m; i++) t[i]+=t[i-1];
		for(int i=n; i; i--) sa[t[f[x[i]]]--]=x[i];
		m=0;
		for(int i=1;i<=n;i++) x[i]=f[i];
		for(int i=1;i<=n;i++)
			if(x[sa[i]]==x[sa[i-1]]&&x[sa[i]+k]==x[sa[i-1]+k])
				f[sa[i]]=m;
			else 
				f[sa[i]]=++m;
		if(m==n) return ;
	}
}
int main() {
	cin>>s+1;
	SA();
	for(int i=1;i<=n;i++)
		cout<<sa[i]<<' ';
}

一些细节:

  1. 注意很多地方都是sa[i],不是i
  2. 注意桶数组t也要开到n的范围,因为f的上限是n。

高度数组有很多作用。

后缀自动机(SAM)

后缀自动机计算出后缀链接树。后缀链接树具有很多性质

#include<iostream>
#include<vector>
using namespace std;
int T[26][2000005],fa[2000005];
long long len[2000005],cnt[2000005];
int top=1,ux=1;
void push(int x) {
	int*t=T[x];
	int u=ux;ux=++top;//注意ux没有int! 
	len[ux]=len[u]+1,cnt[ux]=1;
	while(u&&!t[u]) t[u]=ux,u=fa[u];
	if(!u) fa[ux]=1;
	else {
		int v=t[u];
		if(len[v]==len[u]+1) fa[ux]=v;
		else {
			int vx=++top;
			len[vx]=len[u]+1;//注意这一句话,别忘了 
			fa[ux]=vx,fa[vx]=fa[v],fa[v]=vx;
			while(u&&t[u]==v) t[u]=vx,u=fa[u];
			for(int i=0;i<26;i++) T[i][vx]=T[i][v]; 
		}
	}
}
vector<vector<int>> a;
long long ans;
void dfs(int u) {
	for(auto&v:a[u])
		dfs(v),cnt[u]+=cnt[v];
	if(cnt[u]>1) ans=max(ans,(long long)len[u]*cnt[u]);
}
int main() {
	string s;
	cin>>s;
	for(int i:s) push(i-'a');
	for(int i=0;i<=top;i++) a.push_back({});
	for(int i=2;i<=top;i++)	a[fa[i]].push_back(i);
	dfs(1);
	cout<<ans;
	return 0;
}

代码很简单。
后缀数组和后缀自动机有很多作用是重合的,但也有一些是不重合的。

扩展KMP(Z函数)

KMP算法对两个字符串做精确匹配,exKMP算法则做近似匹配。

#include<iostream>
#include<cstring>
using namespace std;
int z[20000005];
int p[20000005];
char a[20000005];
char b[20000005];
int main() {
	cin>>b>>a;
	int n=strlen(a),m=strlen(b);
	z[0]=n;
	a[++n]='#';
	b[++m]='$';
	for(int i=1,l=0,r=-1;i<n-1;i++) {
		if(i<=r) z[i]=min(z[i-l],r-i+1);
		while(a[z[i]]==a[i+z[i]]) z[i]++;
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
	for(int i=0,l=0,r=-1;i<m-1;i++) {
		if(i<=r) p[i]=min(z[i-l],r-i+1);
		while(p[i]<n-1&&i+p[i]<m-1&&a[p[i]]==b[i+p[i]]) p[i]++;
		if(i+p[i]-1>r) l=i,r=i+p[i]-1;
	}
	long long sum=0;
	for(long long i=0;i<n-1;i++)
		sum^=(i+1)*((long long)z[i]+1);
	cout<<sum<<endl;
	sum=0;
	for(long long i=0;i<m-1;i++)
		sum^=(i+1)*((long long)p[i]+1);	
	cout<<sum<<endl;
	
//	for(int i=0;i<n-1;i++)
//		cout<<z[i]<<' ';
//	cout<<endl;
//	for(int i=0;i<m-1;i++)
//		cout<<p[i]<<' ';
}
  1. 第二次做匹配时while要限制边界条件,防止数组越界。
  2. 注意更新右端点的条件

后记

于是皆大欢喜。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值