【集训】ZR集训——字符串

8.5

异或马拉车 P3501

8.6

ACAM

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;int n;
int trans[N][26];
int cnt=0;
int fail[N];
int to[N];
int insert(string s){
	int now=0;
	for(auto v:s){
		if(!trans[now][v-'a']) trans[now][v-'a']=++cnt;
		now=trans[now][v-'a'];
	}
	return now;
}
vector<int> mp[N];
void Fail(){
	queue<int> q;
	for(int i=0;i<=25;i++){
		if(trans[0][i]) q.push(trans[0][i]);
	}
	while(q.size()){
		int u=q.front();q.pop();
		//cout<<u<<" ";
		for(int i=0;i<=25;i++){
			if(trans[u][i]){
				fail[trans[u][i]]=trans[fail[u]][i];
				q.push(trans[u][i]);
			}
			else trans[u][i]=trans[fail[u]][i];//变成了trie图
		}
	}
	for(int i=1;i<=cnt;i++) mp[fail[i]].push_back(i);
}
int num[N];
void dfs(int u){
	for(auto v:mp[u]){
		dfs(v);
		num[u]+=num[v];
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		string s;cin>>s;
		to[i]=insert(s);
	}
	Fail();
	string s;cin>>s;
	int now=0;
	for(auto v:s){
		now=trans[now][v-'a'];
		num[now]++;
	}
	dfs(0);//暴力跳fail,转差分统计
	for(int i=1;i<=n;i++) cout<<num[to[i]]<<"\n";
	return 0;
}

理解

AC自动机三类指针:trie指针,fail指针,末指针

AC自动机的核心是fail指针建立,后面的query是在其上面做一些其他操作

优化:暴力跳fail,转为拓扑差分求和,这个优化是必须的

AC自动机复杂度分析(trie跳跃),trie树数组大小分析(节点数*字符集大小)

节点有可能要提前继承fail上祖宗的信息(P2444)

trie和fail可以拆开,Trie 树上的祖先节点代表前缀;fail 指针指向的节点代表最长后缀,沿着 fail 链跳能得到所有后缀。

trie树trie图分离,一般用于被迫需要Dfs遍历trie,且与自动机无关(P2414)

应用

AC自动机上dp(P3041):不可以Dfs(因为trie图有环),可以用类似分层图思想刷表转移

反匹配(P2444):trie图找环

PAM

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int trans[N][26];//trie
int fail[N];//fail
int len[N];//长度
int num[N];//答案
int pre=0;//上一个点
int cnt=1;//总数,一定从1开始!!
string s;
int Fail(int x,int tp){
	while(tp-len[x]-1<0||s[tp-len[x]-1]!=s[tp]) x=fail[x];
	return x;
}
void insert(char c,int tp){
	int fa=Fail(pre,tp);
	if(!trans[fa][c-'a']){
		++cnt;
		//更新节点信息
		len[cnt]=len[fa]+2;
		fail[cnt]=trans[Fail(fail[fa],tp)][c-'a'];//Fail(fail[fa],tp)不是fail[fa]
		num[cnt]=num[fail[cnt]]+1;
		trans[fa][c-'a']=cnt;//更新放后面,防止自循环
	}
	pre=trans[fa][c-'a'];
}
int main(){
	cin>>s;
	len[0]=0;len[1]=-1;
	fail[0]=1;fail[1]=1;
	
	int ans=0;
	for(int i=0;i<s.size();i++){
		s[i]=(s[i]-97+ans)%26+97;
		insert(s[i],i);
		ans=num[pre];
		cout<<ans<<" ";
	}
	return 0;
}

最小回文划分(❌️)

Series Link(❌️)

border理论

非常好的border理论笔记

  • 如果一个长度为 \(n\) 的字符串 \(S\) 有一个长度为 \(L\) 的Border,那么 \(n - L\) 就是 \(S\) 的一个周期。

  • \(border(S)=border(maxbd(S))+maxbd(S)\) ,表集合

  • 对于每个 \(u\),连边 \(u->π(u)\),就成了 border 树(或者叫失配树). \(π(u)\)\(u\) 的最长border

上面的东西显而易见

弱周期定理

一个字符串 \(S\),若其有长度为 \(p\)\(q\) 的周期,且 \(p+q≤|S|\),则 \(S\) 有长度为 \(gcd(p,q)\) 的周期 .

根据周期定义和更相减损术证明

强周期定理

一个字符串 \(S\),若其有长度为 \(p\)\(q\) 的周期,且 \(p+q−gcd(p,q)≤|S|\),则 \(S\) 有长度为 \(gcd(p,q)\) 的周期 .

border理论核心推论

一个字符串的所有Border在长度上可以被划分为 \(O(log n)\) 个等差数列

准确来说,将所有 Border 按其二进制最高位进行分段,每一段内的 Border 都构成一个等差数列

8.7

两个都要学

SA

理解

height数组是SA的精髓,也是相比于SAM的优势所在

sa,rk,h,height

参看oi-wiki

基数排序是稳定排序,故而正确

rk是求sa的中间产物

本质不同子串计数:减去sum(height[i])

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char c[N];
int sa[N],rk[N*2];
int tmp[N];
int tot;
int up=131;
int n;
int bin[N];
void sortsa(){//按照第一关键字排序
	memset(bin,0,sizeof bin);
	for(int i=1;i<=n;i++) bin[rk[i]]++;
	for(int i=1;i<=up;i++) bin[i]+=bin[i-1];
	for(int i=n;i>0;i--) sa[bin[rk[tmp[i]]]--]=tmp[i];//保证稳定排序
}
int main(){
	string s;cin>>s;
	n=0;
	for(auto v:s) c[++n]=v;
	for(int i=1;i<=n;i++){
		rk[i]=c[i];
		tmp[i]=i;
	}
	sortsa();
	//rk是离散化后的关键字
	for(int i=1;i<n;i<<=1){
		tot=0;
		//tmp作sa(第二关键字排序)
		for(int j=n-i+1;j<=n;j++) tmp[++tot]=j;
		for(int j=1;j<=n;j++){
			if(sa[j]-i>0) tmp[++tot]=sa[j]-i;
		}
		sortsa();
		//以上可直接替换为sort
		//tmp作rk(重新分配)
		tmp[sa[1]]=up=1;
		
		for(int j=2;j<=n;j++){
			if(rk[sa[j]]!=rk[sa[j-1]]||rk[sa[j]+i]^rk[sa[j-1]+i]) up++;
			tmp[sa[j]]=up;
		}
		memcpy(rk,tmp,sizeof tmp);
	}
	for(int i=1;i<=n;i++) cout<<sa[i]<<" ";
	return 0;
}

height

h递推性质:h[i]≥h[i−1]−1

height 区间最小值定理:任意两个后缀的 LCP 等于它们排名区间内 height 数组的最小值

int height[N];
void getHeight(){
	int k=0;
	for(int i=1;i<=n;i++){
		if(rk[i]==1){
			k=height[rk[i]]=0;
			continue;
		}
		if(k) k--;
		int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&c[i+k]==c[j+k]) k++;
		height[rk[i]]=k;
	}
}

SAM功能比SA丰富许多,但是SA仍有一些优势:

SA相比SAM最核心的不可替代优势是 LCP(最长公共前缀)查询。配合height数组,SA能 O(1) 回答任意后缀的LCP,而SAM需要求Parent树的LCA,实现繁琐得多。

此外,SA还有两大绝对优势:

无视字符集:处理整数、Unicode等大字符集时内存稳定,SAM则可能内存爆炸。

天生有序:直接输出所有后缀的字典序排名,SAM做不到这一点。

而SA掌握以上内容即可,其余建议使用SAM

SAM

link

posted @ 2026-08-05 16:04  Aistyr  阅读(4)  评论(0)    收藏  举报