「学习笔记」AC 自动机

Posted K8He

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「学习笔记」AC 自动机相关的知识,希望对你有一定的参考价值。

「学习笔记」AC 自动机

点击查看目录

前置:「学习笔记」字符串基础:Hash,KMP与Trie

好像对例题的讲解越来越抽象了?

算法

问题

\\(n\\) 个单词在一个长度为 \\(m\\) 的文章里出现过多少个。

思路

很多文章都说这玩意是 Trie 树 + KMP,我觉得确实可以这样理解但是不完全一样。

KMP 有两种理解方式:求 Border 或失配指针,AC 自动机用的是「失配指针」这个理解方式。

KMP 的失配指针指向的是一个最长的与后缀一样的前缀,这样仍然可以继续匹配,而且使需要重新匹配的地方尽量短。

AC 自动机 \\(\\textfail\\) 指针指向的则是一个存在于这个 Trie 树中的最长的与真后缀相同的字符串

依旧是拿 OI-wiki 的图举个例子:

比如单词 she,它的真后缀有 hee \\(\\leftarrow\\) 这个真后缀是空的),其中 he 存在于 Trie 树中,则让 \\(9\\) 号节点的 \\(\\textfail\\) 指针指向最长的 he 的末尾节点 \\(2\\) 号节点。

再如单词 her,它的真后缀有 err ,但是只有 存在于 Trie 树中,则让 \\(3\\) 号节点的 \\(\\textfail\\) 指针指向根节点 \\(0\\)

那么怎么找到 \\(\\textfail\\) 指针呢?

我们设当前节点 \\(p\\) 代表的字符是 \\(c\\),则 \\(p\\)\\(\\textfail\\) 指针应指向 \\(p\\) 的父亲的 \\(\\textfail\\) 指针的代表 \\(c\\) 的儿子。

例如上图中,\\(9\\) 代表的字符是 e\\(9\\) 的父亲是 \\(8\\)\\(8\\)\\(\\textfail\\) 指针指向 \\(1\\)\\(1\\) 的代表 e 的儿子是 \\(2\\),因此 \\(9\\)\\(\\textfail\\) 指针指向 \\(2\\) 号节点。

很好理解吧!xrlong said:没看出来。

但是有个问题,比如图中的六号节点应指向哪里?\\(6\\) 的父亲 \\(5\\)\\(\\textfail\\) 指针 \\(10\\) 的代表 s 的儿子不存在,但是很明显应指向 \\(7\\) 啊!

那就跳到 \\(10\\) 号节点的 \\(\\textfail\\) 指针 \\(0\\),找 \\(0\\) 的代表 s 的儿子 \\(7\\)。但是每次跳很多 \\(\\textfail\\) 指针效率太低了,怎么办?

那就魔改一下这棵树!如果 \\(p\\) 不存在代表 \\(c\\) 的儿子,那就让 \\(p\\) 代表 \\(c\\) 的儿子指向 \\(p\\)\\(\\textfail\\) 指针的代表 \\(c\\) 的儿子。

就像下面这幅图:

最后再次放一下 OI-wiki 上的完整动图:

  1. 蓝色结点:BFS 遍历到的结点 \\(u\\)
  2. 蓝色的边:当前结点下,AC 自动机修改字典树结构连出的边。
  3. 黑色的边:AC 自动机修改字典树结构连出的边。
  4. 红色的边:当前结点求出的 \\(\\textfail\\) 指针。
  5. 黄色的边:\\(\\textfail\\) 指针。
  6. 灰色的边:字典树的边。

代码

namespace ACAUTOMATON 
	class ACAutomaton 
	private:
		ll cnt = 0, nxt[N][26], fail[N], end[N];
	public:
		inline void Clear () 
			cnt = 0;
			memset (nxt, 0, sizeof (nxt));
			memset (end, 0, sizeof (end));
			memset (fail, 0, sizeof (fail));
			return;
		
		inline void Insert (char* s) 
			ll p = 0, len = strlen (s + 1);
			_for (i, 1, len) 
				ll c = s[i] - \'a\';
				if (!nxt[p][c]) nxt[p][c] = ++cnt;
				p = nxt[p][c];
			
			++end[p];
			return;
		
		inline void Build () 
			std::queue <ll> q;
			_for (i, 0, 25) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
			while (!q.empty ()) 
				ll u = q.front (); q.pop ();
				_for (i, 0, 25) 
					if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], q.push (nxt[u][i]);
					else nxt[u][i] = nxt[fail[u]][i];
				
			
			return;
		
		inline ll Query (char* s) 
			ll now = 0, len = strlen (s + 1), ans = 0;
			_for (i, 1, len) 
				now = nxt[now][s[i] - \'a\'];
				for (ll p = now; p && ~end[p]; p = fail[p]) ans += end[p], end[p] = -1;
			
			return ans;
		
	;

例题

板子题。

玄武密码

在每个单词结尾的节点往前跑,看哪个节点深度最高且被访问过。

单词

记录每个点被访问过多少次,但直接记录时间会爆炸。

可以考虑延迟下传访问次数。

病毒

在 trie 树上找一个包括根节点的环,能找到的话直接顺着这个环不断跑就可以构造出无限长的安全代码。

最短母串

用哈希可以随便杀啊!但是这是 AC 自动机题单,所以我要用 AC 自动机写 DP(悲

\\(f_u, sta\\) 表示到节点 \\(u\\) 时,已经经过的字符串状态为 \\(sta\\) 时的最短字符串。

然后不难发现直接暴力广搜转移即可。

文本生成器

\\(f_u, l, b\\) 表示到节点 \\(u\\) 时,已经经过 \\(l\\) 个字符,「是否已经出现过给定串」的答案为 \\(b(b\\in\\0, 1\\)\\) 时的可读文本数量。

直接暴力广搜转移即可。

背单词

首先建出整个 AC 自动机,然后查询每个字符串的答案。

查询的过程有点说不太清,直接看码罢。

注意每次查询时把经过的节点标记一下,只能从标记过的节点转移。

为啥要用线段树啊。

貌似没人有我这个方法?那贴一份代码:

点击查看代码
const ll N = 3e5 + 10;

namespace ACAUTOMATON 
	class ACAutomaton 
	public:
		ll cnt = 0, nxt[N][26], jl[N], fail[N], f[N];
	public:
		inline void Clear () 
			_for (i, 0, cnt) 
				memset (nxt[i], 0, sizeof (nxt[i]));
				fail[i] = f[i] = jl[i] = 0;
			
			cnt = 0;
			return;
		
		inline void Insert (std::string s) 
			ll p = 0, len = s.length () - 1;
			_for (i, 0, len) 
				ll c = s[i] - \'a\';
				if (!nxt[p][c]) nxt[p][c] = ++cnt;
				p = nxt[p][c];
			
			return;
		
		inline void Build () 
			std::queue <ll> q;
			_for (i, 0, 25) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
			while (!q.empty ()) 
				ll u = q.front (); q.pop ();
				_for (i, 0, 25) 
					if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], q.push (nxt[u][i]);
					else nxt[u][i] = nxt[fail[u]][i];
				
			
			return;
		
		inline ll GetAns (std::string s, ll w) 
			ll p = 0, len = s.length () - 1, num = 0;
			_for (i, 0, len) 
				ll c = s[i] - \'a\';
				jl[nxt[p][c]] = 1;
				if (jl[fail[nxt[p][c]]]) f[nxt[p][c]] = std::max (f[nxt[p][c]], f[fail[nxt[p][c]]]);
				num = std::max (num, f[nxt[p][c]]);
				p = nxt[p][c];
			
			return f[p] = std::max (f[p], num + w);
		
	;


namespace SOLVE 
	ll n, m, w[N], ans; std::string s[N];
	ACAUTOMATON::ACAutomaton ac;
	inline ll rnt () 
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c))  if (c == \'-\') w = -1; c = getchar (); 
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	
	inline void In () 
		ac.Clear ();
		n = rnt (), ans = 0;
		_for (i, 1, n) 
			std::cin >> s[i], w[i] = rnt ();
			if (w < 0) continue;
			ac.Insert (s[i]);
		
		return;
	
	inline void Solve () 
		ac.Build ();
		_for (i, 1, n) 
			if (w[i] < 0) continue;
			ans = std::max (ans, ac.GetAns (s[i], w[i]));
		
		return;
	
	inline void Out () 
		printf ("%lld\\n", ans);
		return;
	

密码

首先如果存在一个随意填的位置,那么方案数至少为 \\(52>42\\)。例如:

7 2
good
day

*goodaygooday** 的位置可以填 \\(26\\) 个字母,方案数至少为 \\(2\\times26=52\\)

那么只要不存在随意填的位置,输出就比较方便了。

\\(f_u, l, sta\\) 表示到节点 \\(u\\),字符串长度为 \\(l\\),已经经过的字符串状态为 \\(sta\\) 时的最短字符串,直接暴力广搜转移算出方案数,如果小于 \\(42\\) 就爆搜每种方案即可。

代码比较恶心,贴一下:

点击查看代码
namespace ACAUTOMATON 
	class ACAutomaton 
	private:
		ll cnt = 0, tot = 1, nxt[N][26], fail[N], end[N], f[N][30][M], jl[N][30][M][2];
		class APJifengc  public: ll u, l, s; ;
		std::pair <ll, ll> vis[30 * 45];
		std::vector <ll> answer;
		char temp[N];
	public:
		inline void Insert (char *s, ll id) 
			ll p = 0, len = strlen (s + 1);
			_for (i, 1, len) 
				ll c = s[i] - \'a\';
				if (!nxt[p][c]) nxt[p][c] = ++cnt;
				p = nxt[p][c];
			
			end[p] |= 1 << (id - 1);
			return;
		
		inline void Build () 
			std::queue <ll> q;
			_for (i, 0, 25) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
			while (!q.empty ()) 
				ll u = q.front (); q.pop ();
				_for (i, 0, 25) 
					if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], end[nxt[u][i]] |= end[nxt[fail[u]][i]], q.push (nxt[u][i]);
					else nxt[u][i] = nxt[fail[u]][i];
				
			
			return;
		
		inline ll BFS (ll target,ll m) 
			std::queue <APJifengc> q;
			ll ans = 0; f[0][0][0] = 1;
			q.push ((APJifengc)0, 0, 0);
			while (!q.empty ()) 
				ll u = q.front ().u, l = q.front ().l, s = q.front ().s; q.pop ();
				if (l > m) break;
				if (s == target && l == m) ans += f[u][l][s];
				_for (i, 0, 25) 
					ll v = nxt[u][i], ln = l + 1, st = s | end[v];
					if (!f[v][ln][st]) q.push ((APJifengc)v, ln, st);
					f[v][ln][st] += f[u][l][s];
				
			
			return ans;
		
		inline ll DFS (ll u, ll l, ll s, ll target, ll m) 
			if (jl[u][l][s][0]) return jl[u][l][s][1];
			jl[u][l][s][0] = 1;
			if (l == m) return jl[u][l][s][1] = (s == target);
			_for (i, 0, 25) jl[u][l][s][1] |= DFS (nxt[u][i], l + 1, s | end[nxt[u][i]], target, m);
			return jl[u][l][s][1];
		
		inline void PrintAns (ll u, ll l, ll s, ll m) 
			if (!jl[u][l][s][1]) return;
			if (l == m)  puts (temp + 1); return; 
			_for (i, 0, 25) temp[l + 1] = i + \'a\', PrintAns (nxt[u][i], l + 1, s | end[nxt[u][i]], m);
			return;
		
	;


namespace SOLVE 
	ll n, m, ans; char s[20];
	ACAUTOMATON::ACAutomaton ac;
	inline ll rnt () 
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c))  if (c == \'-\') w = -1; c = getchar (); 
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	
	inline void In () 
		m = rnt (), n = rnt ();
		_for (i, 1, n) 
			scanf ("%s", s + 1);
			ac.Insert (s, i);
		
		return;
	
	inline void Solve () 
		ac.Build ();
		ans = ac.BFS ((1 << n) - 1, m);
		if (ans <= 42) ac.DFS (0, 0, 0, (1 << n) - 1, m);
		return;
	
	inline void Out () 
		printf ("%lld\\n", ans);
		if (ans <= 42) ac.PrintAns (0, 0, 0, m);
		return;
	

禁忌

有点像 GT 考试

\\(f_i, u\\) 表示长度为 \\(i\\),到了节点 \\(u\\) 的串的期望伤害。

\\[f_i, u = \\frac1alphabet\\sum_son_v,c = u f_i - 1,v \\]

但是 \\(len\\le10^9\\),不能直接转移。

于是套一下矩阵乘法就好了。

码:

点击查看代码
namespace MATRIX 
	class Matrix 
	private:
		ll n; ldb a[N][N];
	public:
		inline ldb* operator [] (ll x)  return a[x]; 
		inline void Init (ll nn)  n = nn, memset (a, 0, sizeof (a)); return; 
		inline Matrix operator * (Matrix another) const 
			Matrix ans; ans.Init (n);
			_for (i, 0, n) _for (j, 0, n) _for (k, 0, n)
				ans[i][j] += a[i][k] * another[k][j];
			return ans;
		
		inline void Print () 
			printf ("%lld\\n", n);
			_for (i, 0, n)  _for (j, 0, n) printf ("%Lf ", a[i][j]); puts (""); 
			puts ("");
			return;
		
	;


namespace ACAUTOMATON 
	class ACAutomaton 
	private:
		ll cnt = 0, nxt[N][26], fail[N], end[N];
	public:
		inline void Insert (std::string s) 
			ll p = 0, len = s.length () - 1;
			_for (i, 0, len) 
				ll c = s[i] - \'a\';
				if (!nxt[p][c]) nxt[p][c] = ++cnt;
				p = nxt[p][c];
			
			end[p] = 1;
			return;
		
		inline ll Build (ll alphabet) 
			std::queue <ll> q;
			_for (i, 0, alphabet - 1) if (nxt[0][i]) fail[nxt[0][i]] = 0, q.push (nxt[0][i]);
			while (!q.empty ()) 
				ll u = q.front (); q.pop ();
				_for (i, 0, alphabet - 1) 
					if (nxt[u][i]) fail[nxt[u][i]] = nxt[fail[u]][i], q.push (nxt[u][i]);
					else nxt[u][i] = nxt[fail[u]][i];
				
				end[u] |= end[fail[u]];
			
			return cnt;
		
		inline MATRIX::Matrix GetMatrix (ll alphabet) 
			MATRIX::Matrix ma; ma.Init (cnt + 1);
			_for (i, 0, cnt) 
				_for (j, 0, alphabet - 1) 
					if (end[nxt[i][j]]) ma[i][0] += 1.0 / (ldb)(alphabet), ma[i][cnt + 1] += 1.0 / (ldb)(alphabet);
					else ma[i][nxt[i][j]] += 1.0 / (ldb)(alphabet);
				
			
			ma[cnt + 1][cnt + 1] = 1.0;
			return ma;
		
	;


namespace SOLVE 
	ll n, m, len, alphabet;
	std::string s[N];
	MATRIX::Matrix ans;
	ACAUTOMATON::ACAutomaton ac;
	inline ll rnt () 
		ll x = 0, w = 1; char c = getchar ();
		while (!isdigit (c))  if (c == \'-\') w = -1; c = getchar (); 
		while (isdigit (c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar ();
		return x * w;
	

	inline MATRIX::Matrix FastPow (MATRIX::Matrix a, ll b) 
		MATRIX::Matrix an; an.Init (m);
		_for (i, 0, m) an[i][i] = 1.0;
		while (b) 
			if (b & 1) an = an * a;
			a = a * a, b >>= 1;
		
		return an;
	

	inline void In () 
		n = rnt (), len = rnt (), alphabet = rnt ();
		_for (i, 1, n) 
			std::cin >> s[i];
			ac.Insert (s[i]);
		
		return;
	
	inline void Solve () 
		m = ac.Build (alphabet) + 1;
		MATRIX::Matrix ma = ac.GetMatrix (alphabet);
		ans.Init (m), ans[0][0] = 1.0;
		ma = FastPow (ma, len), ans = ans * ma;
		return;
	
	inline void Out () 
		printf ("%.10Lf\\n", ans[0][m]);
		return;
	

\\[\\Huge\\mathfrakThe\\ End \\]

AC自动机算法学习

KMP+TRIE

int val[1000100][31],tot;
int tr[1000100];
int fail[1000100];
struct AC_Trie{
    void clean(){
        tot=0;
        memset(val,0,sizeof(val));
        memset(tr,0,sizeof(tr));
        memset(fail,0,sizeof(fail));
    }
    void build(){
        queue<int> q;
        memset(fail,0,sizeof(fail));
        while(!q.empty()) q.pop();
        for(int i=0;i<26;i++) if(val[0][i]!=0) q.push(val[0][i]);
        while(!q.empty()){
            int u=q.front();q.pop();
            for(int i=0;i<26;i++){
                if(val[u][i]!=0){
                    fail[val[u][i]]=val[fail[u]][i];
                    q.push(val[u][i]); 
                }else{
                    val[u][i]=val[fail[u]][i];
                }
            }
        }
    }
    void insert(string x){
        int len=x.length(),p=0;
        for(int i=0;i<len;i++){
            int c=x[i]-'a';
            if(val[p][c]==0) tot++,val[p][c]=tot;
            p=val[p][c];
        }
        tr[p]++;
    }
    int find(string x){
        int len=x.length(),p=0,res=0;
        for(int i=0;i<len;i++){
            p=val[p][x[i]-'a'];
            for(int j=p;j&&~tr[j];j=fail[j]) res+=tr[j],tr[j]=-1;
        }
        return res;
    }
}tree;

以上是关于「学习笔记」AC 自动机的主要内容,如果未能解决你的问题,请参考以下文章

AC算法学习笔记

AC自动机--summer-work之我连模板题都做不出

AC自动机笔记

Apm飞控学习笔记-AC_PosControl位置控制-Cxm

Apm飞控学习笔记-AC_PosControl位置控制-Cxm

Apm飞控学习笔记-AC_PosControl位置控制-Cxm