AC自动机
多重匹配求连续子串问题
AC自动机入门题
对于字符串匹配可以用kmp,但是对于多个字符串匹配呢
用ac自动机
也就是kmp+trie
ac自动机
模式串he,she,him,hers,shit构成的trie树
然后去查询fail指针
fail指针的理解:
是把下层的去指向上层
而对于上层的子串必定是下层的子串,所有说如果能匹配下层,必定能匹配上层(利用这个减少时间复杂度)
第一层(根下面的一层)肯定是指向根的
非第一层的,如果满足不了上述则也指向根,表示该子串和前面的子串没有公共前缀,要重新开始匹配
模板
- 对子字符串进行构建trie树
- 求失配指针
- 用主串进行匹配
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn = 1e6 + 5;
int cnt = 0;//trie的指针
struct tree{
int fail;//失配指针
int vis[26];//子结点的位置
int end;//标记有几个单词以这个节点结尾
}ac[maxn];//trie树
void bulid(char *t){//对字典树进行初始化
int len = strlen(t);
int now = 0;//字典树当前的指针
for(int i = 0; i < len; i++){
if(ac[now].vis[t[i]-'a'] == 0){//trie树里没有这个子结点
ac[now].vis[t[i]-'a'] = ++cnt;
}
now = ac[now].vis[t[i]-'a'];//向下构造
}
ac[now].end += 1;
}
void get_fail(){//构建fail指针
queue<int>q;
for(int i = 0; i < 26; i++){//对第一层的进行处理,全部指向根(第0层)
if(ac[0].vis[i] != 0){
ac[ac[0].vis[i]].fail = 0;
q.push(ac[0].vis[i]);//同时把第一层的所有子结点压入队列里
}
}
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = 0; i < 26; i++){
if(ac[u].vis[i] != 0){//存在这个子结点
ac[ac[u].vis[i]].fail = ac[ac[u].fail].vis[i];
q.push(ac[u].vis[i]);
}else{//不存在子结点
ac[u].vis[i] = ac[ac[u].fail].vis[i];
}
}
}
}
int ac_query(char *s){
int len = strlen(s);
int now = 0, ans = 0;
for(int i = 0; i < len; i++){
now = ac[now].vis[s[i] - 'a'];
for(int t = now; t && ac[t].end != -1; t = ac[t].fail){
ans += ac[t].end;//只有单词都匹配到,且匹配到结尾才会加非零数
ac[t].end = -1;
}
}
return ans;
}
int main(){
int n;
scanf("%d", &n);
char s[maxn];//主串
char t[maxn];//子串
for(int i = 0; i < n; i++){
scanf("%s", t);//子串
bulid(t);
}
ac[0].fail = 0;//结束标记
get_fail();
scanf("%s", s);//主串
printf("%d\n", ac_query(s));
return 0;
}
优化版模板
- 根的失配是根
- 先把所有根没指向的字母的失配指针指向根---初始化
- 根指向的字母的的下一个字母失配指向根---初始化,且把下一个字母放入队列
- 循环操作,寻找失配指针
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=1e6+5;
struct ac_auto{
int fail[maxn];//失配指针
int vis[maxn][26];//子结点的位置
int end[maxn];//标记有几个单词以这个节点结尾
int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
int newNode(){
for(int i=0;i<26;i++){//26叉树
vis[L][i]=-1;
}
end[L++]=0;
return L-1;//返回节点编号
}
void initial(){
L=0;
root=newNode();//把第一行清空了,root=0,L=1
}
void Insert(char *t){
int len=strlen(t);
int now=root;
for(int i=0;i<len;i++){
int x=t[i]-'a';
if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
}
end[now]++;
}
void get_fail(){
queue<int>q;
fail[root]=root;//根指向根
for(int i=0;i<26;i++){//对于根结点下的第一排字母(单词的首个字母)
if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
else{//存在这个字母
fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
q.push(vis[root][i]);//再把这个字母的下一个字母放进去
}
}
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<26;i++){
if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
else{
fail[vis[now][i]]=vis[fail[now]][i];
q.push(vis[now][i]);
}
}
}
}
int Query(char *s){
int now=root;
int ans=0;
int len=strlen(s);
for(int i=0;i<len;i++){
now=vis[now][s[i]-'a'];
int temp=now;
while(temp!=root){
ans+=end[temp];
end[temp]=0;
temp=fail[temp];
}
}
return ans;
}
}ac;
int t,n;
char s[maxn];
char str[maxn];
int main(){
scanf("%d",&t);
while(t--){
scanf("%d",&n);
ac.initial();
for(int i=0;i<n;i++){
scanf("%s",s);
ac.Insert(s);
}
ac.get_fail();
scanf("%s",s);
printf("%d\n",ac.Query(s));
}
}
题目
记录哪几个单词出现过
/*
和模板比,就加了个used来记录这个单词是否被记录过
且把end改成了记录结点而不是记录几次出现,(对于每一个字典树的点,只记录了唯一的一个数值)
同时把26个字母改成了128个
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=1e6+5;
struct ac_auto{
int fail[maxn];//失配指针
int vis[maxn][128];//子结点的位置
int end[maxn];//标记有几个单词以这个节点结尾
int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
bool used[510];
int newNode(){
for(int i=0;i<128;i++){//26叉树
vis[L][i]=-1;
}
end[L++]=0;
return L-1;//返回节点编号
}
void initial(){
L=0;
root=newNode();//把第一行清空了,root=0,L=1
}
void Insert(char *t,int id){
int len=strlen(t);
int now=root;
for(int i=0;i<len;i++){
int x=t[i];//直接赋值为ASCII码
if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
}
end[now]=id;//now的值是唯一的
}
void get_fail(){
queue<int>q;
fail[root]=root;//根指向根
for(int i=0;i<128;i++){//对于根结点下的第一排字母(单词的首个字母)
if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
else{//存在这个字母
fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
q.push(vis[root][i]);//再把这个字母的下一个字母放进去
}
}
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<128;i++){
if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
else{
fail[vis[now][i]]=vis[fail[now]][i];
q.push(vis[now][i]);
}
}
}
}
int Queue(char *s){
memset(used,false,sizeof(used));
int now=root;
int ans=0;
int len=strlen(s);
for(int i=0;i<len;i++){
int x = s[i];
now=vis[now][x];
int temp=now;
while(temp!=root){
if(end[temp]!=0){
used[end[temp]]=true;
ans =1;
}
temp=fail[temp];
}
}
return ans;
}
}ac;
int t,n;
char s[maxn];
char str[maxn];
int main(){
int m;
scanf("%d",&m);
ac.initial();
for(int i=1;i<=m;i++){
scanf("%s",str);
ac.Insert(str,i);
}
ac.get_fail();
int n;
cin>>n;
int ans=0;
for(int i=1;i<=n;i++){
scanf("%s",s);
if(ac.Queue(s)!=0){
printf("web %d:",i);
for(int i=1;i<=m;i++){
if(ac.used[i]!=false){
printf(" %d",i);
}
}
ans++;
putchar('\n');
}
}
printf("total: %d\n",ans);
}
每个单词出现的次数,可重叠
/*
和模板比
end改成了记录结点而不是记录几次出现,(对于每一个字典树的点,只记录了唯一的一个数值)
同时把26个字母改成了128个
而且把单词编号加入了Hash来寻找哪个单词,并且记录出现的次数
还有一个记录字符串的结构体
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=2e6+5;
int Hash[1005];
struct node{
char ss[55];
}words[1005];
struct ac_auto{
int fail[maxn];//失配指针
int vis[maxn][128];//子结点的位置
int end[maxn];//标记有几个单词以这个节点结尾
int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
int newNode(){
for(int i=0;i<128;i++){//26叉树
vis[L][i]=-1;
}
end[L++]=0;
return L-1;//返回节点编号
}
void initial(){
L=0;
root=newNode();//把第一行清空了,root=0,L=1
}
void Insert(char *t,int id){
int len=strlen(t);
int now=root;
for(int i=0;i<len;i++){
int x=t[i];//直接赋值为ASCII码
if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
}
end[now]=id;
}
void get_fail(){
queue<int>q;
fail[root]=root;//根指向根
for(int i=0;i<128;i++){//对于根结点下的第一排字母(单词的首个字母)
if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
else{//存在这个字母
fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
q.push(vis[root][i]);//再把这个字母的下一个字母放进去
}
}
while(!q.empty()){
int now=q.front();
q.pop();
for(int i=0;i<128;i++){
if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
else{
fail[vis[now][i]]=vis[fail[now]][i];
q.push(vis[now][i]);
}
}
}
}
void Query(char *s){
int now=root;
int ans=0;
int len=strlen(s);
for(int i=0;i<len;i++){
int x = s[i];
now=vis[now][x];
int temp=now;
while(temp!=root){
if(end[temp]!=0){
Hash[end[temp]]++;
}
temp=fail[temp];
}
}
}
}ac;
int t,n;
char s[maxn];
int main(){
int m;
while(cin>>m){
ac.initial();
memset(Hash,0,sizeof(Hash));
for(int i=1;i<=m;i++){
scanf("%s",words[i].ss);
ac.Insert(words[i].ss,i);
}
ac.get_fail();
scanf("%s",s);
ac.Query(s);
for(int i=1;i<=m;i++){
if(Hash[i]!=0){
printf("%s: %d\n",words[i].ss,Hash[i]);
}
}
}
}
I‘m Stein, welcome to my blog