KMP,trie,AC自动机,topo优化

\(KMP\)

前缀函数

给定一个长度为 \(n\) 的字符串 \(s\) ,其前缀函数定义为一个长度为 \(n\) 的数组 \(\pi\)
其中 \(\pi[i]\) 的定义为:

  1. 如果子串 \(s[0...i]\) 有一对相等的真前缀 \(s[0...k-1]\) 与真后缀 \(s[i-k+1...i]\) ,则 \(\pi[i]\)就是这个真前(后)缀的长度,即 \(\pi[i]=k\)
  2. 若子串 \(s[0...i]\) 有不止一对相等的真前缀与真后缀,则 \(\pi[i]\) 为其中最长的那一对的长度。
  3. 若没有相等的真前缀与真后缀,则 \(\pi[i]=0\)

综上: \(\pi[i]=\max\limits_{k=0}^{i}\{k|s[0...k-1]=s[i-k+1...i]\}\)

前缀函数的求法--\(KMP\)

很容易想到 \(O(n^3)\) 的做法,即枚举每个 \(k\) ,显然这不是理想的做法。
Knuth,Morris,Pratt在1977年共同发布了一个算法,简记为 \(KMP\) 算法。
求前缀函数为该算法的一个典型应用。
基本思想:
假设当前求解到 \(s[i]\)
显然,\(s[i-1]=s[\pi[i-1]]\),若 \(s[i]=s[\pi[i-1]+1]\)\(\pi[i]=\pi[i-1]+1\)
\(\overbrace{s_1 s_2 s_3 ... } ^{0...\pi[i-1]} s_4 s_5... \overbrace{s_1 s_2 s_3 ...} ^{^{i-\pi[i]-1...i-1}}s_4\)
那么若 \(s[i]\ne s[\pi[i-1]+1]\) 又如何呢?
同上 \(s[i-1]=s[\pi[i-1]]=s[\pi[\pi[i-1]]]=...=s[\pi^j[i-1]]\),我们只需继续向前跳 \(\pi\) 数组即可。
\(\overbrace{ \underbrace{s_1 s_2 s_3 ...}_{0...\pi[\pi[i-1]]} s_4 s_1 s_2 s_3 ...}^{0...\pi[i-1]} s_5 \overbrace{s_1 s_2 s_3...}^{^{i-\pi[i-1]-1...i-1}} s_4\)
实现代码:

for(int i=1,j=0;i<s.length();i++){
    j=nxt[i-1];
    while(j&&s[j]!=s[i])j=nxt[j-1];
    j+=(s[j]==s[i]);nxt[i]=j;
}
for(int i=0;i<s.length();i++)cout<<nxt[i]<<' ';cout<<endl;

KMP代码的应用

给定两个个字符串 \(s,t\) ,长度分别为 \(n,m\)
\(s\)\(t\) 中出现多少次。
\(s+'\#'+t\) 跑一遍 \(KMP\) ,其中 \(\#\)\(s,t\) 中均未出现的字符, \(\pi[i]=n\) 的位置即为 \(s[n-1]\)\(t\) 中出现的位置。统计即可。

cin>>s>>t;
n=s.length();
s=s+"#"+t;
for(int i=1,j=0;i<s.length();++i){
    j=nxt[i-1];
    while(j&&s[j]!=s[i])j=nxt[j-1];
    j+=(s[j]==s[i]);nxt[i]=j;
}
for(int i=0;i<s.length();++i){
    if(nxt[i]==n)++ans;
}

\(Trie\)

为了存储大量字符串,我们可以建一棵树。
其中红色点为字符串结束标志,标记此节点与其祖先构成了一个字符串。
下图的 \(Trie\) 储存了 \(xie,xor,he,her,his\) 五个字符串。

很容易发现两字符串最长公共字串长度即为两字符串结尾节点的 \(lca\) 的深度。

void insert(char *s,int len){
	int u=0,c;
	for(int i=1;i<=len;++i){
		c=s[i]-'a';
		if(!ch[u][c])ch[u][c]=++cnt;
		u=ch[u][c];
	}
	++b[u];//字符串的末尾,不同的题使用不同的记录法
}

AC自动机

AC自动机是一种将 \(KMP\)\(trie\) 的算法。

insert函数

即为 \(trie\) 的插入。

build函数

即为求AC自动机的后缀数组fail。

queue<int>q;
void build(){
	for(int i=0;i<26;i++)
        if(ch[0][i])q.push(ch[0][i]);
	while(!q.empty()){
		int u=q.front();
        q.pop();
		for(int i=0;i<26;i++)
            if(ch[u][i]){
                fail[ch[u][i]]=ch[fail[u]][i];
                q.push(ch[u][i]);
            }
            else ch[u][i]=ch[fail[u]][i];//----------------优化,省略了从u跳到fail[u]的时间
	}
	return;
}

query函数

int query(char *t,int l) {
	int u=0,c;
	for(int i=1;i<=l;i++){
		c=t[i]-'a';
		u=ch[u][c];
		for(int j=u;j;j=fail[j])
            ans+=b[j];
	}
	return ans;
}

\(topo\) 优化

基于P3808
在AC自动机查询答案跳fail的时候,有的时候会跳很多遍。
把fail数组当作边,使用 \(topo\) 排序,直接在 \(topo\) 的时候将答案传递。

build函数

queue<int>q;
void build(){
	for(int i=0;i<26;i++)
        if(ch[0][i])q.push(ch[0][i]);
	while(!q.empty()){
		int u=q.front();
        q.pop();
		for(int i=0;i<26;i++)
            if(ch[u][i]){
                fail[ch[u][i]]=ch[fail[u]][i];
                ++indeg[fail[ch[u][i]];
                q.push(ch[u][i]);
            }
            else ch[u][i]=ch[fail[u]][i];
	}
}

query函数

void query(){
	int u=0,c;
	for(int i=1;i<=l;i++){
		c=t[i]-'a';
		u=ch[u][c];
        ans[u]+=b[j];//----------------不用跳了
	}
}

topo函数统计答案

int Ans;
void topo(){
	for(int i=1;i<=cnt;i++)if(indeg[i]==0)q.push(i);
	while(!q.empty()){
		int u=q.front();q.pop();
		if(u)ans[fail[u]]+=ans[u];
		Ans=max(Ans,ans[fail[u]]);
		if(!(--indeg[fail[u]]))q.push(fail[u]);
	}
}
posted @ 2024-07-16 19:49  Xie2Yue  阅读(25)  评论(0)    收藏  举报