字符串匹配算法之AC自动机总结

零.胡扯

AC自动机? 自动AC机?别想多了,他只是一种字符串算法而已

一个搞笑的举报贴,还是举报我的

好好好,进入主题

一.问题引入

我们知道kmp,哈希等等都是能够做单字符串匹配的

但是如果是多个串去匹配一个串呢?

先看个模板题 【模板】AC自动机

Q:能否做n次KMP?
A:可以是可以,但是你想想会不会TLE?
Q:那此题怎么做?
A:AC自动机?
Q:啥玩野啊
A:那就给你讲讲吧

二.算法概述

前置技能: T r i e + K M P Trie+KMP Trie+KMP

感性理解:AC自动机= T r i e + K M P Trie+KMP Trie+KMP

AC自动机是以Trie树为基础,结合KMP的思想建立的,常被用于多模式串的字符串匹配。

建立AC自动机分为两个步骤:

1.把所有的模式串构建在 T r i e Trie Trie

2.对 T r i e Trie Trie树的每一个节点构建失配指针 f a i l fail fail

接下来我们分开考虑

三.构建Trie树

你Trie树怎么建AC自动机就怎么建

建完之后在尾部打个标记表示此处一个串结束了,方便答案统计

void insert(char *s) {
		int n = strlen(s), now = 0 ;
		for (int i = 0; i < n; i++) {
			int x = s[i] - 'a' ;
			if (!trie[now][x]) trie[now][x] = ++cnt ;
			now = trie[now][x] ;
		}
		e[now]++ ;
	}

四.构造失配( f a i l fail fail)指针

fail指针和kmp的next数组有很大的相似之处,两者同样是在失配的时候用于跳转的指针。

但是由于算法的目的不同,他们也有不同点:KMP要求的是后缀和前缀匹配,而AC自动机只需要相同后缀即可。

下面介绍 f a i l fail fail指针的基础思想

构建 f a i l fail fail 指针,可以参考 K M P KMP KMP中构造 n e x t next next数组的思想。

我们利用部分已经求出 f a i l fail fail 指针的结点推导出当前结点的 f a i l fail fail指针。具体我们用BFS实现:

考虑字典树中当前的节点 u u u u u u的父节点是 p p p p p p通过字符 c c c的边指向 u u u

假设深度小于 u u u的所有节点的 f a i l fail fail指针都已求得。那么 p p p f a i l fail fail指针显然也已求得。

我们跳转到 p p p f a i l fail fail指针指向的结点 f a i l [ p ] fail[p] fail[p]

如果结点 f a i l [ p ] fail[p] fail[p] 通过字母 c c c 连接到的子结点 w w w 存在:

则让 u u u f a i l fail fail指针指向这个结点 w w w f a i l [ u ] = w fail[u]=w fail[u]=w)。

相当于在 p p p f a i l [ p ] fail[p] fail[p] 后面加一个字符 c c c,就构成了 f a i l [ u ] fail[u] fail[u]

如果 f a i l [ p ] fail[p] fail[p]通过字母 c c c 连接到的子结点 w w w 不存在:

那么我们继续找到 f a i l [ f a i l [ p ] ] fail[fail[p]] fail[fail[p]] 指针指向的结点,重复上述判断过程,一直跳 f a i l fail fail 指针直到根节点。

如果真的没有,就令 f a i l [ u ] = r o o t fail[u]=root fail[u]=root

如此即完成了 f a i l fail fail指针的构建。

在构建 f a i l fail fail 指针的同时,我们也对 T r i e Trie Trie中模式串的结尾构建 f a i l fail fail 指针。这样在匹配到结尾后能自动跳转到下一个匹配项。

五.代码剖析

先看是如何通过BFS求出 f a i l fail fail指针的

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

Q:为何不能直接把root丢进队列里?
A:如果这样干,根节点的子节点的 f a i l fail fail指针就变成了本身了
Q:为何没有 w h i l e while while就直接付给了 t r i e trie trie啊?
A: 字典树转字典图,能够更加方便的记录答案

再看怎么查询

int query(char *t) {
		int now = 0, res = 0, n = strlen(t) ;
		for (int i = 0; i < n; i++) {
			now = trie[now][t[i] - 'a'] ;
			for (int j = now; j && ~e[j]; j = fail[j]) res += e[j], e[j] = -1 ;
		}
		return res ;
	}

声明 p p p作为字典树上当前匹配到的结点, r e s res res即返回的答案
循环遍历匹配串, p p p在字典树上跟踪当前字符。
利用 f a i l fail fail 指针找出所有匹配的模式串,累加到答案中。然后清 0 0 0
e [ j ] e[j] e[j] 取反的操作用来判断 e [ j ] e[j] e[j] 是否等于-1。

六.总结

到此,你已经理解了整个AC自动机的内容。我们一句话总结AC自动机的运行原理: 构建字典图实现自动跳转,构建失配指针实现多模式匹配。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std ;
#define rep(i, a, b) for (int (i) = (a); (i) <= (b); (i)++)
#define per(i, a, b) for (int (i) = (a); (i) >= (b); (i)--)
#define clr(a) memset(a, 0, sizeof(a))
#define ass(a, sum) memset(a, sum, sizeof(a))
#define lowbit(x) (x & -x)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define enter cout << endl
#define siz(x) ((int)x.size())
typedef long long ll ;
typedef unsigned long long ull ;
typedef vector <int> vi ;
typedef pair <int, int> pii ;
typedef pair <ll, ll> pll ;
typedef map <int, int> mii ;
typedef map <string, int> msi ;
const int N = 1000010 ;
const int INF = 0x3f3f3f3f ;
const int iinf = 1e9 ;
const ll linf = 2e18 ;
const int MOD = 1000000007 ;
void print(int x) { cout << x << endl ; exit(0) ; }
void PRINT(string x) { cout << x << endl ; exit(0) ; }
void douout(double x){ printf("%lf\n", x + 0.0000000001) ; }

char s[N] ;
int T ;

struct ac {
	int trie[N][26], e[N], fail[N], cnt = 0 ;
	void insert(char *s) {
		int n = strlen(s), now = 0 ;
		for (int i = 0; i < n; i++) {
			int x = s[i] - 'a' ;
			if (!trie[now][x]) trie[now][x] = ++cnt ;
			now = trie[now][x] ;
		}
		e[now]++ ;
	}
	void build() {
		queue <int> q ;
		clr(fail) ;
		for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
		while (!q.empty()) {
			int now = q.front() ; q.pop() ;
			for (int i = 0; i < 26; i++) 
			if (trie[now][i]){
				fail[trie[now][i]] = trie[fail[now]][i] ;
				q.push(trie[now][i]) ;
			} else trie[now][i] = trie[fail[now]][i] ;
		}
	}
	int query(char *t) {
		int now = 0, res = 0, n = strlen(t) ;
		for (int i = 0; i < n; i++) {
			now = trie[now][t[i] - 'a'] ;
			for (int j = now; j && ~e[j]; j = fail[j]) res += e[j], e[j] = -1 ;
		}
		return res ;
	} 
} HQG_AC ;

signed main() {
	freopen("ac.in", "r", stdin) ;
	freopen("ac.out", "w", stdout) ;
	scanf("%d", &T) ;	
	while (T--) {
		scanf("%s", s) ;
		HQG_AC.insert(s) ;
	}
	HQG_AC.build() ;
	scanf("%s", s) ; printf("%d\n", HQG_AC.query(s)) ;
}

七.例题

刚刚的那个模板题的加强版

也还是差不多的,在结尾上标记编号,然后查询时如果 n o w now now是结尾,那么就答案+1,然后就排序输出即可,别忘清零

int n ;
char s[M][N], t[N] ; 

struct node {
	int pos, num ;
	friend bool operator < (const node &a, const node &b) {
		if (a.num != b.num) return a.num > b.num ;
		else return a.pos < b.pos ;
	}
} ans[N] ;

struct AC {
	int trie[N][26], fail[N], end[N], cnt = 0 ;
	void init(int x) {
		clr(trie[x]) ;
		fail[x] = 0, end[x] = 0 ;
	}
	void insert(char *s, int id) {
		int now = 0, n = strlen(s) ;
		for (int i = 0; i < n; i++) {
			int c = s[i] - 'a' ;
			if (!trie[now][c]) trie[now][c] = ++cnt, init(cnt) ; 
			now = trie[now][c] ;
		}
		end[now] = id ; // flag 
	}
	void build() {
		queue <int> q ; clr(fail) ;
		for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
		while (!q.empty()) {
			int now = q.front() ; q.pop() ;
			for (int i = 0; i < 26; i++)
			if (trie[now][i]) {
				fail[trie[now][i]] = trie[fail[now]][i] ;
				q.push(trie[now][i]) ;
			} else trie[now][i] = trie[fail[now]][i] ;
		}
	}
	void query(char *s) {
		int now = 0, n = strlen(s) ;
		for (int i = 0; i < n; i++) {
			int c = s[i] - 'a' ;
			now = trie[now][c] ;
			for (int j = now; j; j = fail[j]) ans[end[j]].num++ ; // calculate each strings 
		}
	}
} my ; 


signed main() {
	while (scanf("%d", &n) && n) {
		my.cnt = 0 ; my.init(0) ;
		for (int i = 1; i <= n; i++) {
			scanf("%s", s[i]) ;
			ans[i] = (node) {i, 0} ;
			my.insert(s[i], i) ;
		}
		my.build() ;
		scanf("%s", t) ; my.query(t) ;
		sort(ans + 1, ans + n + 1) ;
		cout << ans[1].num << "\n" ; cout << s[ans[1].pos] << "\n" ;
		for (int i = 2; i <= n; i++) 
		if (ans[i].num == ans[1].num) cout << s[ans[i].pos] << "\n" ;
		else break ;
	}
}

之后,这个题也就是银组的升级版

在结尾打个长度标记

然后栈操作。。。根那个题目差不多的吧,就是把 K M P − > A C KMP->AC KMP>AC自动机

标记哪些有用哪些没用

int st[N], what[N] ;
int top, n ;
char s[N], t[N] ; 

struct AC {
	int trie[N][26], fail[N], end[N], cnt = 0 ;
	void insert(char *s) {
		int now = 0, n = strlen(s) ;
		for (int i = 0; i < n; i++) {
			int c = s[i] - 'a' ;
			if (!trie[now][c]) trie[now][c] = ++cnt ;
			now = trie[now][c] ;
		}
		end[now] = n ;
	} 
	void build() {
		queue <int> q ; clr(fail) ;
 		for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
		while (!q.empty()) {
			int now = q.front() ; q.pop() ;
			for (int i = 0; i < 26; i++) 
			if (trie[now][i]){
				fail[trie[now][i]] = trie[fail[now]][i] ;
				q.push(trie[now][i]) ;
			} else trie[now][i] = trie[fail[now]][i] ;
		}
	} 
	void solve(char *s) {
		int now = 0, n = strlen(s) ;
		for (int i = 0; i < n; i++) {
			int c = s[i] - 'a' ;
			now = trie[now][c] ;
			st[++top] = now ;
			what[top] = i ;
			if (end[now]) { // it is one of the matching strings’end
				top -= end[now] ; 
				if (!top) now = 0 ;
				else now = st[top] ;
			}
		}
	}
} HQG ;

signed main() {
	scanf("%s", s) ; 
	scanf("%d", &n) ; 
	for (int j = 1; j <= n; j++) {
		scanf("%s", t) ;
		HQG.insert(t) ;
	}
	HQG.build() ; HQG.solve(s) ; 
	for (int i = 1; i <= top; i++) cout << s[what[i]] ; enter ;
}

[TJOI2013]单词

多串去匹配多串? 你想多了

此题需要对 f a i l fail fail指针更加深入的了解

我们发现一个串出现的次数就是 f a i l fail fail树中该点的子树和

于是有了这个结论就可以过了

char s[N] ;
int sum ;

struct AC {
	int trie[N][26], fail[N], a[N], h[N], sz[N], cnt ;
	void insert(char *s, int x) {
		int now = 0, n = strlen(s) ;
		for (int i = 0; i < n; i++) {
			int c = s[i] - 'a' ;
			if (!trie[now][c]) trie[now][c] = ++cnt ; 
			now = trie[now][c] ;
			sz[now]++ ;
		}
		a[x] = now ;
	}
	void build() {
	    int hd = 0, tl = 0 ;
		clr(fail) ;
		for (int i = 0; i < 26; i++) if (trie[0][i]) h[++tl] = trie[0][i] ;
		while (hd < tl) {
			int now = h[++hd] ;
			for (int i = 0; i < 26; i++) 
			if (trie[now][i]) {
				fail[trie[now][i]] = trie[fail[now]][i] ;
				h[++tl] = trie[now][i] ;
			} else trie[now][i] = trie[fail[now]][i] ;
		}
	}
	void solve() {
	    for (int i = cnt; i >= 0; i--) sz[fail[h[i]]] += sz[h[i]] ;
	    for (int i = 1; i <= sum; i++) printf("%d\n", sz[a[i]]) ;
	}
} my ;

signed main() {
	scanf("%d", &sum) ;
	for (int i = 1; i <= sum; i++) {
		scanf("%s", s) ;
		my.insert(s, i) ; 
	}
	my.build() ; my.solve() ;
}

覆盖字符串

搞个差分数组就没了

原题这样就可以过了,这个题目听说要10~50个串后重建AC自动机,还是蛮巧妙的

原题AC的代码

int cf[N] ;
char s[N], t[N] ;
int n, m ;

struct AC {
	int trie[N][26], fail[N], end[N], val[N], cnt = 0 ;
	void insert(char *s) {
		int now = 0, n = strlen(s) ;
		for (int i = 0; i < n; i++) {
			int c = s[i] - 'a' ;
			if (!trie[now][c]) trie[now][c] = ++cnt ;
			now = trie[now][c] ;
		}
		val[now] = n ;
	}
	void build() {
		queue <int> q ; clr(fail) ;
		for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
		while (!q.empty()) {
			int now = q.front() ; q.pop() ;
			for (int i = 0; i < 26; i++)
			if (trie[now][i]) {
				int x = trie[now][i] ;
				fail[x] = trie[fail[now]][i] ;
				end[x] = val[fail[x]] ? fail[x] : end[fail[x]] ;
				q.push(x) ;
			} else trie[now][i] = trie[fail[now]][i] ;
		}
	}
	int query(char *t) {
		int now = 0, n = strlen(t) ;
		for (int i = 0; i < n; i++) {
			int c = t[i] - 'a' ;
			while (now && !trie[now][c]) now = fail[now] ;
			now = trie[now][c] ;
			if (val[now]) cf[i - val[now] + 1]++, cf[i + 1]-- ; // make cf array 
			else cf[i - val[end[now]] + 1]++, cf[i + 1]-- ; 
		}
	}
} my ; 

signed main() {
	scanf("%d", &n) ; scanf("%s", s) ; scanf("%d", &m) ;
	for (int i = 1; i <= m; i++) {
		scanf("%s", t) ;
		my.insert(t) ;
	}
	my.build() ; my.query(s) ;
	int now = 0, ans = 0 ;
	for (int i = 0; i < n; i++) {
		now += cf[i] ;
		if (now) ans++ ;
	} 
	printf("%d\n", n - ans) ;
}

还有些题,我会慢慢放的

你可能感兴趣的:(AC自动机,算法总结)