https://blog.youkuaiyun.com/bestsort/article/details/82947639
https://www.cnblogs.com/cmmdc/p/7337611.html
关于ac自动机,原理还没有很懂,我先就目前的理解说一说我理解的原理,等更彻底的理解之后再来修改补充。
在匹配过程中,如果找到了一个模式串,就把这个串的fail指针指向的其所有的后缀串全部计算进去(应该是这样。。),如果失配了,也要找当前串的存在的后缀串并计算进去
关于建立Trie树,很简单,但是要记录一下这个单词在建立的时候有几次
fail指针,采用bfs的方法:(关于手写队列和stl都可以)
1、第一层节点要特殊处理,使第一层节点的fail指针全部指向根节点,然后将第一层节点全部加入队列
2、一个节点出队列,然后将其存在的子节点的fail指针指向父节点的fail指针指向的节点的具有相同子节点的节点((有点绕是吧。。)可以看一下上面第一篇文章,里面的图片说明很直观容易理解,)但是要注意,如果父节点fail指向的节点子节点没有与之相同的节点,那就按照这个规律继续向上去找,如果到根节点都找不到,就说明不存在,那么就将这个节点的fail指针直接指向root
3、每一层都重复这个操作,直到Trie的所有节点全部都建立了fail指针
//AC自动机
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <map>
#include <string>
#include <sstream>
using namespace std;
const int Size = 26;
const int MAXL = 55;
const int MAXl = 1000000 + 5;
struct Trie
{
int count;//前缀出现次数;
int sum;//这个节点是不是一个单词的结尾,并记录这个单词在建树的时候出现了几次
Trie *next[Size];//孩子节点数目
Trie *fail;
//bool isEnd;//判断到该点时,是否是一个单词
//string name;
};
char pat[MAXL],text[MAXl];
int ans;
Trie *root;//建立根节点;
void init(Trie *node)
{
node->count = node->sum = 0;
memset(node->next,NULL,sizeof node->next);
node->fail = NULL;
//node->isEnd = false;
}
void Insert(const char *s)
{
int len = strlen(s);
int pos;
Trie *p = root;
for(int i = 0; i < len; i++){
pos = s[i] - 'a';
if(p->next[pos] == NULL){
//如果这个字符在这个节点没有储存过,就建立一个新节点来储存
p->next[pos] = new Trie;
init(p->next[pos]);
}
p = p->next[pos];
p->count++;
}
p->sum++;//表明当前位置为一个单词的节点
}
void build_fail_pointer()
{
queue <Trie *> q;
Trie *p = root;
//将第一层存在的节点全部指向root节点
for(int i = 0; i < Size; i++){
if(p->next[i] != NULL){
p->next[i]->fail = root;
q.push(p->next[i]);
}
}
while(!q.empty()){
Trie *temp = q.front();
q.pop();
//依次回溯该节点的父节点的失败指针直到某节点的next[i]与该节点相同,
//则把该节点的失败指针指向该next[i]节点;
//若回溯到 root 都没有找到,则该节点的失败指针指向 root
for(int i = 0; i < Size; i++){
if(temp->next[i] != NULL){
Trie *node = temp->fail;//将该结点的父结点的失败指针给node
while(node != NULL){
if(node->next[i] != NULL){
temp->next[i]->fail = node->next[i];
break;
}
node = node->fail;
}
if(node == NULL){
//如果这个节点的fail指针匹配失败,那么就指向根节点
temp->next[i]->fail = root;
}
q.push(temp->next[i]);
}
}
}
}
void find_ac_automaton()
{
Trie *p = root;
int pos;
int len = strlen(text);
for(int i =0; i < len; i++){
pos = text[i] - 'a';
while(p->next[pos] == NULL && p != root){
//如果失配且下一个fail节点不是根节点,就往上找
p = p->fail;
}
//找到上级节点的fail指针指向的节点是否含有当前字符
p = p->next[pos];
if(p == NULL){
//如果没找到,就返回根节点从新开始找
p = root;
}
Trie *tep = p;
//向上查找fail指针指向的字符串,这些字符串的结尾都是这个字符(个人理解)
//并把出现的模式串数量全部加进去
while(tep != root && tep->sum != -1){
ans += tep->sum;
tep->sum = -1;//防止重复计算
tep = tep->fail;
}
}
}
void delTrie(Trie *root)
{
for(int i = 0; i < Size; i++){
if(root->next[i] != NULL){
delTrie(root->next[i]);
}
}
delete root;
}
int main()
{
int t,n;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
root = new Trie;
init(root);
while(n--){
scanf("%s",pat);
Insert(pat);
}
scanf("%s",text);
build_fail_pointer();
ans = 0;
find_ac_automaton();
printf("%d\n",ans);
delTrie(root);
}
return 0;
}
hdu 5880
这个题,咋说呢,简单模板,但是做起来就跟神仙题一样,究极离谱(给自己的菜找借口),指针方法内存存爆了,也不知道为啥爆了,先贴一下吧,顺便学学数组的ac自动机操作。。
如果用指针内存爆了的问题解决了,再说这个代码是不是一定正确吧(狗头)
//AC自动机
//指针
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <map>
#include <string>
#include <sstream>
using namespace std;
const int Size = 26;
const int MAXN = 1e6 + 10;
struct Trie
{
//int count;//前缀出现次数;
int sum;//这个节点是不是一个单词的结尾,并记录这个单词在建树的时候出现了几次
Trie *next[Size];//孩子节点数目
Trie *fail;
//bool isEnd;//判断到该点时,是否是一个单词
//string name;
}trie[MAXN];
char pat[MAXN],text[MAXN];
int cnt = 0;
Trie *root;//建立根节点;
void init(Trie *node)
{
node->sum = 0;
memset(node->next,NULL,sizeof node->next);
node->fail = NULL;
//node->isEnd = false;
}
Trie *Newndoe()
{
Trie *p = &trie[cnt++];
init(p);
return p;
}
void Insert(const char *s)
{
int len = strlen(s);
int pos;
Trie *p = root;
for(int i = 0; i < len; i++){
pos = s[i] - 'a';
if(p->next[pos] == NULL){
//如果这个字符在这个节点没有储存过,就建立一个新节点来储存
p->next[pos] = Newndoe();
}
p = p->next[pos];
}
p->sum = len;//记录模式串长度,方便转变成'*'
}
void build_fail_pointer()
{
queue <Trie *> q;
Trie *p = root;
//将第一层存在的节点全部指向root节点
for(int i = 0; i < Size; i++){
if(p->next[i] != NULL){
p->next[i]->fail = root;
q.push(p->next[i]);
}
}
while(!q.empty()){
Trie *temp = q.front();
q.pop();
//依次回溯该节点的父节点的失败指针直到某节点的next[i]与该节点相同,
//则把该节点的失败指针指向该next[i]节点;
//若回溯到 root 都没有找到,则该节点的失败指针指向 root
for(int i = 0; i < Size; i++){
if(temp->next[i] != NULL){
Trie *node = temp->fail;//将该结点的父结点的失败指针给node
while(node != NULL){
if(node->next[i] != NULL){
temp->next[i]->fail = node->next[i];
break;
}
node = node->fail;
}
if(node == NULL){
//如果这个节点的fail指针匹配失败,那么就指向根节点
temp->next[i]->fail = root;
}
q.push(temp->next[i]);
}
}
}
}
void find_ac_automaton()
{
Trie *p = root;
int pos;
int len = strlen(text);
for(int i =0; i < len; i++){
if(text[i] >= 'A' && text[i] <= 'Z'){
pos = text[i] - 'A';
}
else if(text[i] >= 'a' && text[i] <= 'z'){
pos = text[i] - 'a';
}
else{
continue;
}
while(p->next[pos] == NULL && p != root){
//如果失配且下一个fail节点不是根节点,就往上找
p = p->fail;
}
//找到上级节点的fail指针指向的节点是否含有当前字符
p = p->next[pos];
if(p == NULL){
//如果没找到,就返回根节点从新开始找
p = root;
}
Trie *tep = p;
//向上查找fail指针指向的字符串,这些字符串的结尾都是这个字符
//并把出现的模式串数量全部加进去
while(tep != root){//需要把所有出现的模式串都和谐掉,所以不需要去重操作
if(tep->sum){
for(int k = i; k > i - tep->sum; k--){
text[k] = '*';
}
break;//第一个后缀一定是最长的,不需要再转移到下一个fail指针
}
tep = tep->fail;
}
}
}
/*void delTrie(Trie *root)
{
for(int i = 0; i < Size; i++){
if(root->next[i] != NULL){
delTrie(root->next[i]);
}
}
delete root;
}*/
int main()
{
int t,n;
scanf("%d",&t);
while(t--){
cnt = 0;
scanf("%d",&n);
root = Newndoe();
while(n--){
scanf("%s",pat);
Insert(pat);
}
cin.get();
gets(text);
build_fail_pointer();
find_ac_automaton();
puts(text);
/*for(int i = 0; i < cnt; i++){
trie[i].sum = 0;
for(int j = 0; j < Size; j++){
trie[i].next[j] = NULL;
}
trie[i].fail = NULL;
}*/
}
return 0;
}
对于数组的建立fail指针,为什么一步就可以呢?因为对于没有出现的字符,它会把父节点fail指针指向的节点的与其字符相同的子节点存起来,这样就省略的如果当前节点存在,但是其父节点fail指针指向的节点与之相同字符的子节点不存在,就会向上找的过程,因为上层节点在建立fail指针的时候,已经给你预处理好了,所以只需要直接取父节点fail指针指向的节点的具有相同字符的值就可以,如果其值是指向根节点的,那就说明,当前要建立fail指针的节点,也指向根节点
//hdu 5880
//数组,ac自动机
#include <iostream>
#include <string>
#include <cstring>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <queue>
using namespace std;
const int MAXN = 1e6 + 10;
const int Size = 26;
int fail[MAXN],trie[MAXN][Size],val[MAXN],nNode;
//失败指针,字典树(数组实现),记录模式串长度,子典树节点
char s[MAXN];
int tran(char ch)
{
return tolower(ch) - 'a';
}
int newNode()
{
for(int i = 0; i < Size; i++){
trie[nNode][i] = 0;
fail[i] = 0;
}
val[nNode] = 0;
return nNode++;
}
void Insert(const char *s)
{
int u = 0;
int len = strlen(s);
for(int i = 0; i < len; i++){
if(!trie[u][tran(s[i])]){
trie[u][tran(s[i])] = newNode();
}
u = trie[u][tran(s[i])];
}
//在结尾处记录模式串长度
val[u] = len;
}
void build_ac_fail_pointer()
{
queue <int> q;
fail[0] = 0;
for(int i = 0; i < Size; i++){
if(trie[0][i]){
fail[trie[0][i]] = 0;//第一层fail指针指向根结点
q.push(trie[0][i]);
}
else{
fail[trie[0][i]] = 0;
}
}
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = 0; i < Size; i++){
if(trie[u][i]){
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else{
//向上找上级,找到的第一个用fail指针指向,但是如果没有上级满足
//那么肯定其父节点fail指针的 ℹ 节点对应的值为0,就不用向指针方法一样不断向上找了
//上面同理
trie[u][i] = trie[fail[u]][i];
}
}
}
}
//替换字符
void Query(char *s)
{
int len = strlen(s);
int u = 0;
for(int i = 0; i < len; i++){
//判断是否为字符,是则返回非0不是返回0
if( !isalpha ( s[i] ) ){
//如果不是字符,那么就从根结点从新开始找
u = 0;
continue;
}
//如果是字符,那么就直接取最大后缀,如果没有不存在,那么长度肯定为0
u = trie[u][tran(s[i])];
for(int j = u; j != 0; j = fail[j]){
if(val[j]){
for(int k = i; k > i - val[j]; k--){
s[k] = '*';
}
}
}
}
}
int main()
{
int t;
scanf("%d",&t);
while(t--){
nNode = 0;
newNode();
int n;
scanf("%d",&n);
while(n--){
scanf("%s",s);
Insert(s);
}
build_ac_fail_pointer();
getchar();
//linux下不能使用gets。。使用fgets替代,但是不知道为啥就tle了
//cin.get(s,MAXN );
// fgets(s,MAXN - 5,stdin);
gets(s);
Query(s);
puts(s);
}
return 0;
}
洛谷3808
模板题
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <string>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 1000005;
const int Size = 26;
int trie[MAXN][Size],fail[MAXN * Size],cnt[MAXN],nNode;
int ans;
string s;
int newnode()
{
for(int i = 0; i < Size; i++){
trie[nNode][i] = 0;
}
fail[nNode] = 0;
cnt[nNode] = 0;
return nNode++;
}
void Insert(const string &s)
{
int len = s.size();
int u = 0;
for(int i = 0; i < len; i++){
int pos = s[i] - 'a';
if( !trie[u][pos] ){
trie[u][pos] = newnode();
}
u = trie[u][pos];
}
//记录该字符串出现次数
cnt[u]++;
}
void build_ac_fail_pointer()
{
queue <int> q;
fail[0] = 0;
for(int i = 0; i < Size; i++){
if( trie[0][i] ){
fail[trie[0][i]] = 0;
q.push(trie[0][i]);
}
else{
fail[trie[0][i]] = 0;
}
}
while ( !q.empty())
{
int u = q.front();
q.pop();
for(int i= 0; i < Size; i++){
if( trie[u][i] ){
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else{
trie[u][i] = trie[fail[u]][i];
}
}
}
}
void Query(const string &s)
{
int len = s.size();
int u = 0;
for(int i = 0; i < len; i++){
int pos = s[i] - 'a';
u = trie[u][pos];
for(int j = u; j && cnt[j] != -1; j = fail[j]){
ans += cnt[j];
cnt[j] = -1;
}
}
}
int main()
{
int n;
cin>>n;
newnode();//很重要,因为根节点是0,如果没有这一步,插入的时候,会从根节点新建节点
while(n--){
cin>>s;
Insert(s);
}
build_ac_fail_pointer();
cin>>s;
Query(s);
cout<<ans<<endl;
return 0;
}
洛谷3796
模板题,先把输入的字符按照输入的顺序编号(建议不要从0开始编号,个人认为会比较麻烦)
然后在建树的时候,在字符串尾部位置标记上我们给定的序号
在扫描模式串的时候,只要这个序号出现一次,那么相应的字符串出现次数则加一,最后求出出现次数最多的字符串即可
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <string>
#include <cmath>
#include <cstdlib>
using namespace std;
const int MAXN = 1000005;
const int Size = 26;
int trie[MAXN][Size],fail[MAXN],cnt[MAXN],nNode,vis[MAXN];
string s[151],T;
void init()
{
memset(trie,0,sizeof(trie));
memset(fail,0,sizeof(fail));
memset(cnt,0,sizeof(cnt));
memset(vis,0,sizeof(vis));
nNode = 1;
}
int newnode()
{
for(int i = 0; i < Size; i++){
trie[nNode][i] = 0;
}
fail[nNode] = 0;
cnt[nNode] = 0;
return nNode++;
}
void Insert(const string &s,int i)
{
int len = s.size();
int u = 0;
for(int i = 0; i < len; i++){
int pos = s[i] - 'a';
if( !trie[u][pos] ){
trie[u][pos] = newnode();
}
u = trie[u][pos];
}
//记录该字符串的标号
cnt[u] = i;
}
void build_ac_fail_pointer()
{
queue <int> q;
fail[0] = 0;
for(int i = 0; i < Size; i++){
if( trie[0][i] ){
fail[trie[0][i]] = 0;
q.push(trie[0][i]);
}
else{
fail[trie[0][i]] = 0;
}
}
while ( !q.empty())
{
int u = q.front();
q.pop();
for(int i= 0; i < Size; i++){
if( trie[u][i] ){
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else{
trie[u][i] = trie[fail[u]][i];
}
}
}
}
void Query(const string &s)
{
int len = s.size();
int u = 0;
for(int i = 0; i < len; i++){
int pos = s[i] - 'a';
u = trie[u][pos];
for(int j = u; j; j = fail[j]){
if(cnt[j]){
vis[cnt[j]]++;//如果有模式串标记,更新出现次数
}
}
}
}
int main()
{
int n;
while(cin>>n,n){
init();
//newnode();//很重要,因为根节点是0,如果没有这一步,插入的时候,会从根节点新建节点
for(int i = 1; i <= n; i++){
cin>>s[i];
Insert(s[i],i);//利用输入字符串的顺序来给字符串标序,也可以用map
}
build_ac_fail_pointer();
cin>>T;
Query(T);
int ans = 0;
for(int i = 1; i <= n; i++){
ans = max(ans,vis[i]);
}
cout<<ans<<endl;
for(int i = 1; i <= n; i++){
if(vis[i] == ans){
cout<<s[i]<<endl;
}
}
}
return 0;
}