【手敲算法】AC自动机 从 理解 到 裸敲

为什么要学习 AC自动机

当 众多(n 个) "模式串 Ti" 需要确定 与 "母串S" 的相对关系时,
如果 使用 kmp 逐个进行 模式串Ti,与S串(lenS = n)匹配的匹配工作,单次复杂度 = O(n)
n个 Ti,则 复杂度 = O(n * n),此时需要 AC自动机,进行 "一对多" 的快速匹配
复杂度 = O(n +m) m是字符失配跳动的次数(小)

  • 相对关系:匹配了哪些 Ti串,匹配多少条 Ti串,匹配Ti 串多少次,等等 可以想到的种种变型算法

算法原理

AC自动机的 "fail指针" 与kmp的 "next数组" 有异曲同工之妙
next数组,是在失配后 直接跳到某个index下,因为kmp只有一条单链的字符串,可以直接用下标表示。
AC自动机,因为是树状结构,只能通过指针  来进行失配后的跳动操作。

  • 第一步:建立字典树(随机数据,后边举例会制造特殊数据)

    【手敲算法】AC自动机 从 理解 到 裸敲_第1张图片

使用 "指针 + 结构体" 的方法,构建字典树

  • 第二步:建立 失配指针fail

【手敲算法】AC自动机 从 理解 到 裸敲_第2张图片
   情况一:所有  "与root直接相连"节点 的fail指针 指向 "root"
   情况二:其余节点分如下情况,进行fail指针的拓展(假设已知如下情况的fail指针)

【手敲算法】AC自动机 从 理解 到 裸敲_第3张图片
       拓展情况(1)下个节点  "黄圈b" or "黄圈c",他们可以在 "当前节点的 fail链上" 找到某个节点 "x"(x = 某节点),
                               这个节点 "x" 也同样拓展了 b节点  或者  c节点,让"黄圈b" 指向 "x->b" ,"黄圈c" 指向 "x->c"
                               (注: x 可能未必如图所示同时拥有 b  and c,可能是 x1->b ,x2->c , x1和x2 在同一条fail链上)
【手敲算法】AC自动机 从 理解 到 裸敲_第4张图片

       拓展情况(2)如图所示,当前节点的  下个 "节点d" ,在当前节点的fail链上 没有一个 "x->d"
                               那么 "节点d" 会直接指向 root节点

  • 第三步:AC自动机匹配——"S串,在n个匹配串Ti 构建的树上进行匹配"

每次拿出 一个S[i] (i from 0 to len-1) 在字典树上匹配
    匹配成功:使用匹配节点内的数据,对ans进行按要求的更新
       (比如:(1)计算匹配次数:每次成功看当前 "节点x" 是不是一个Ti 的结尾字符,true则 ans++。
                     (2)输出匹配了哪些串:每个 "Ti 结尾节点" 记录"Ti的i",成功匹配一个结尾节点,将其内部 i push_back进 vector ans)
    匹配失败:向  当前节点fail链  上方跳动,直到root截止。 

尝试题目

HDU-2896  HDU-3065 

代码

#include 
#define fi first
#define se second
#define pb push_back
#define Clear( x , y ) memset( x , y , sizeof(x) );
#define Qcin() std::ios::sync_with_stdio(false);
using namespace std;
const int maxn = (int)5e5 + 7;
const int inf = 1e9 + 7;
typedef long long ll;
typedef pair  PII;
int n, m;


const int over = 130;
int cnt;
struct node{
    node *next[over];
    node *fail;
	int sum, S_id;
	node(){
		//memset(next , NULL , sizeof(next) );
		for(int i = 0 ; i < over ; i++){
			next[i] = NULL;
		}
		fail = NULL;
		sum = 0;
	}
};
node *root , *newnode;
char key[210];
char pattern[Maxn];
int head,tail;
 
void Insert(char *s , int num) {
	node *p = root;
	for(int i = 0; s[i]; i++) {
		//int x = s[i] - 'a';
		int x = s[i];
		if(p->next[x] == NULL) {
			newnode = new node;
			p->next[x]=newnode;
		}
		p = p->next[x];
	}
	p->S_id = num;
}
 
queue  q;
void build_fail_pointer() {
	q.push(root);
	node *p;
	node *temp;
	while(!q.empty()) {
		temp = q.front(); q.pop();
		for(int i = 0; i < over; i++) {
			if(temp->next[i]) {
				if(temp == root) {
					temp->next[i]->fail = root;
				} else {
					p = temp->fail;
					while(p) {
						if(p->next[i]) {
							temp->next[i]->fail = p->next[i];
							break;
						}
						p = p->fail;
					}
					if(p == NULL) temp->next[i]->fail = root;
				}
				q.push(temp->next[i]);
			}
		}
	}
}
 
vector  ans;
void ac_automation(char *ch)
{
	node *p = root;
	int len = strlen(ch);
	for(int i = 0; i < len; i++) {
		//int x = ch[i] - 'a';
		int x = ch[i];
		while(!p->next[x] && p != root) p = p->fail;
		p = p->next[x];
		if(!p) p = root;
		node *temp = p;
		while(temp != root) {
			if(temp->S_id > 0) { // 条件更改处
				ans.push_back(temp->S_id);
				//cnt += temp->sum;
				//temp->sum = -1;
			}
			else break;
			temp = temp->fail;
		}
	}
}
int main()
{
	root = new node;
	scanf("%d",&n);
	for(int i = 1; i <= n ; i++) {
		scanf(" %[^\n]",&key);
		Insert(key , i);
	}
	build_fail_pointer();
	int Q; scanf(" %d",&Q);
	cnt = 0;
	for(int i = 1 ; i <= Q ; i++){
		scanf(" %[^\n]",&pattern);
		ans.clear();
		ac_automation(pattern);
		if(ans.size()){
			sort(ans.begin() , ans.end());
			cnt++;
			printf("web %d:",i);
			for(int i = 0 ; i < ans.size() ; i++){
				printf(" %d",ans[i]);
			}
			printf("\n");
		}
	}
	printf("total: %d\n",cnt);
	return 0;
}

 

你可能感兴趣的:(手敲算法)