必要前置技能:Trie
非必要前置技能:KMP
有 n n n个模式串,一个文本串,求有多少个模式串在文本串中出现过。
暴力有两种方式:第一种是用所有模式串用 KMP 与文本串匹配,第二种是对所有模式串建一棵Trie,然后对于每个文本串的前缀在Trie中统计结果。AC自动机对第二种方法进行了优化。
洛咕P3808 【模板】AC自动机(简单版)
类似于Trie,AC自动机中的点表示状态,边表示当下一个字符是这条边对应的字符时转移到什么状态。
建好AC自动机后,根据文本串在自动机中搜索并统计信息,得出答案。
AC自动机的构造方法:先按 n n n个模式串建一棵Trie。如果只建Trie,那么就是暴力了。考虑匹配时的过程。失配时我们不想放弃前面匹配过的结果,所以考虑怎样保留当前匹配的结果(这里与kmp有点像)。
令失配指针 f a i l fail fail表示最长的与当前状态的后缀相同,且不为自身的字符串对应的状态。当失配时,就往 f a i l fail fail上跳,这样就保证这个状态在文本串中出现过,且是不失配的最长的状态。
令 x x x的 f a i l fail fail链表示 x x x不断往 f a i l [ x ] fail[x] fail[x]跳的过程中出现的所有节点组成的链。
举个例子,假设 n = 4 n=4 n=4,字符串都只有AB两种字符, 4 4 4个文本串分别为"AA",“AB”,“B”,“BA”。
先建出Trie:
“A"这个状态显然不会失配(因为无论下一个字符是A还是B都可以继续匹配)。
“AA"这个状态,最长的与它后缀相同且不为它本身的状态为"A”。所以它的失配指针指向"A"这个状态。
“AB"这个状态,最长的与它后缀相同且不为它本身的状态为"B”。所以它的失配指针指向"B"这个状态。
“B"这个状态,最长的与它后缀相同且不为它本身的状态为””(空字符串)。所以它的失配指针指向根节点。
“BA"这个状态,最长的与它后缀相同且不为它本身的状态为"A”。所以它的失配指针指向"A"这个状态。
用红色箭头表示 f a i l fail fail指针的指向,可以得到这样的AC自动机:
是不是记录下模式串结尾的位置,然后根据Trie树和失配指针走就珂以了呢?
模拟一下,比如"BAA"这个字符串:
第一个字符B,可以匹配,从根节点走到"B"这个状态。发现"B"这个节点是一个模式串的结尾,所以答案+1。
第二个字符A,可以匹配,从"B"状态走到"BA"状态。发现"BA"这个节点是一个模式串的结尾,所以答案+1。
第三个字符A,不能匹配,从"BA"状态跳到失配指针处,即"A"状态。显然这个跳到的状态"A"在文本串中出现过(第一个A)。第三个字符是A,所以从"A"状态走到"AA"状态,发现"AA"这个节点是一个模式串的结尾,答案+1。
最后我们得到的答案是3,而"BAA"这个字符串中包含了"B"、“BA"和"AA"这3个字符串。
完美!
……
……
……真的完美吗?
不妨考虑一下文本串为"AB"时会怎么走。不难发现它只会统计到一个模式串,就是"AB”。但显然文本串"AB"包含了模式串"B",而"B"并没有统计到。
发现若有一个模式串被这个状态包含,这种方法是统计不到的。考虑失配指针表示的是最长的与当前状态的后缀相同,且不为自身的状态。
所以每次搜索到一个状态时,沿着它的fail链不断跳上去(就是不断地跳到这个状态的失配指针处),统计fail链上有多少模式串即珂。
建AC自动机时,先建普通的Trie。算失配指针 f a i l fail fail时,可以用bfs的方法,自根节点向下遍历,用父亲节点的 f a i l fail fail推出孩子的 f a i l fail fail。
具体见代码:
int n,tot=1;
struct node {
int ch[27];
} w[Size];
int word[Size]; //word[i]表示i号节点上有多少个模式串的结尾
void insert(char *str) {
int len=strlen(str),rt=1;
for(re i=0; i<len; i++) {
int p=str[i]-'a';
rt=w[rt].ch[p]?w[rt].ch[p]:w[rt].ch[p]=++tot;
}
word[rt]++;
}
int fail[Size],Queue[Size];
void bfs() { //通过bfs建AC自动机
for(re i=0; i<26; i++) w[0].ch[i]=1;
int hd=0,tl=0;
Queue[++tl]=1;
while(hd<tl) {
int x=Queue[++hd];
for(re i=0; i<26; i++) {
//这里对求fail的部分进行了优化
//本来求fail的方法是沿着fail链往上跳,找到第一个能匹配i+'a'字符的状态
//这里如果一个状态无法匹配某个字符
//我们把它的这个字符对应的孩子改为fail链上最长的能匹配这个字符的状态
//这样就能保证跳到的一定是能匹配这个字符的状态
if(w[x].ch[i]) {
fail[w[x].ch[i]]=w[fail[x]].ch[i];
Queue[++tl]=w[x].ch[i];
} else {
w[x].ch[i]=w[fail[x]].ch[i];
}
}
}
}
查询时每次需要沿着 f a i l fail fail链往上跳。为了防止重复统计,用一个vis数组保存某个节点以及它的 f a i l fail fail链是否被统计过了。具体见代码。
bool vis[Size];
int query(char *str) {
int len=strlen(str),rt=1,ans=0;
for(re i=0; i<len; i++) {
rt=w[rt].ch[str[i]-'a'];
for(re j=rt; j && !vis[j]; j=fail[j]) {
//如果前面统计过j(即vis[j]==true),那么fail链上剩下的就不需统计了
vis[j]=true;
ans+=word[j];
}
}
return ans;
}
然后洛咕P3808就珂以A了qwq。
洛咕P3966 [TJOI2013]单词
题意:一篇由 n n n个小写字母组成的单词组成的文章,问对于每个单词,它在文章中多少个地方出现了。
发现若 y y y是 x x x的 f a i l fail fail链上的节点,则 y y y的状态一定是 x x x的状态的后缀。
题目求的是某个单词在多少个地方出现,即它是多少个状态的后缀。也就是说,需要求的是某个单词在多少个状态的 f a i l fail fail链上。
因为一个节点的 f a i l fail fail链上的节点的bfs序都比它小,所以建出AC自动机,然后按照bfs序来把自身的值叠加到 f a i l fail fail处即可。
#include
#include
#include
#include
#define re register int
#define mod 1000000007
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
inline void write(const int x) {
if(x>9) write(x/10);
putchar(x%10+'0');
}
const int Size=1000005;
const int INF=0x3f3f3f3f;
int n,tot=1,id[Size];
char now[Size];
struct node {
int ch[27];
int cnt;
int fail;
} w[Size];
void insert(char *str,int num) {
int rt=1,len=strlen(str);
w[rt].cnt++;
for(re i=0; i<len; i++) {
int p=str[i]-'a';
rt=w[rt].ch[p]?w[rt].ch[p]:w[rt].ch[p]=++tot;
w[rt].cnt++;
}
id[num]=rt;
}
int Queue[Size],sum[Size];
void getfail() {
for(re i=0; i<26; i++) w[0].ch[i]=1;
int hd=0,tl=0;
Queue[++tl]=1;
while(hd<tl) {
int x=Queue[++hd];
for(re i=0; i<26; i++) {
if(w[x].ch[i]) {
w[w[x].ch[i]].fail=w[w[x].fail].ch[i];
Queue[++tl]=w[x].ch[i];
} else {
w[x].ch[i]=w[w[x].fail].ch[i];
}
}
}
for(re i=tl; i; i--) {
w[w[Queue[i]].fail].cnt+=w[Queue[i]].cnt;
}
}
//#define I_love_Chtholly
int main() {
#ifdef I_love_Chtholly
freopen("data.txt","r",stdin);
freopen("WA.txt","w",stdout);
#endif
n=read();
for(re i=1; i<=n; i++) {
scanf("%s",now);
insert(now,i);
}
getfail();
for(re i=1; i<=n; i++) {
write(w[id[i]].cnt);
putchar(10);
}
return 0;
}