字符串算法——AC自动机讲解

本博客由南昌理工学院ACM集训队赞助播出 教练要求

AC自动机

文字说明

本算法需要先掌握字典树和KMP算法后再来学习
如果没学过其中的任意一种,请先去学习再来

  • 用我见过的大佬引用:AC自动机简单来说就是Tire Tree + 看毛片 KMP,也就是在树上看毛片 KMP
  • AC自动机用来解决多模式串匹配,也就是给好几个子串,一个很长很长的母串,让你处理一些问题,比如什么子串出现的次数或种类之类的。
    先用字典树建立树,然后用fail指针将子串串起来,和KMP中的next数组很像,我们定义fail数组,fail[i]为与以i节点为结尾的串的后缀有最大公共长度的前缀的结尾编号。
    是不是看不懂,我也是orz
    还是看图吧

图片分析

在这里插入图片描述
首先对she,shr,say,her建立字典树,
对于以d结尾的子串she,不存在任何一个其他子串的前缀与she匹配,但he与的she后缀he匹配,这是匹配到的最长情况,于是这个e上的fail指针就指向he上的e。然后,如果没有找到任何一个前缀与当前串的任何一个后缀一样的话,那么fail只能指到根了。和KMP的原理一样,fail跳到的地方必然这个子串的前面不用再比较了,因为根据fail的定义,它的已经前缀已经被比较过并且匹配了。只用再往后比就行了

代码解析

// 建立字典树模板
	void build() {
		int len = tmp.length();
		int u = 0;
		for(int i = 0; i < len; ++i) {
			int s = tmp[i] - 'a'; // 清晰一点这里先赋值字母
			if(tr[u].ch[s] == 0)
				tr[u].ch[s] = ++cnt;
			u = tr[u].ch[s];
		}
		tr[u].num++; // 这里表示每一个子串结束
	}
	// 建立fail指针 用链表串起来
	void get_fail() {
		queue<int> q;
		for(int i = 0; i < 26; ++i) {
			if(tr[0].ch[i]) { // 找到有字目的点将fail指针指向根
				tr[tr[0].ch[i]].fail = 0;
				q.push(tr[0].ch[i]); // 进队
			}
		}
		while(!q.empty()) { // 队列为不为空
			int u = q.front();
			q.pop();
			for(int i = 0; i < 26; ++i) {
				if(tr[u].ch[i]) { // 判断当前点的子树字母的点有某有
					tr[tr[u].ch[i]].fail = tr[tr[u].fail].ch[i]; // 将当前子树点的fail赋给当前点fail指向点的同一个字母
					q.push(tr[u].ch[i]); // 进队
				} else {
					tr[u].ch[i] = tr[tr[u].fail].ch[i]; 
					// 某有 就让这个子树点直接指向当前点fail指向点的同一个字母
				}
			}
		}
	}
	// 最后就是求啦
	int ac() {
		int len = tmp.length();
		int u = 0, ans = 0;
		for(int i = 0; i < len; ++i) { // 母串
			int s = tmp[i]-'a'; // 清晰一点这里先赋值字母
			u = tr[u].ch[s]; // 当前最长子串
			int v = u;
			while(v && tr[v].num != -1) { // 开始找每一个子串啦
				ans += tr[v].num; 
				tr[v].num = -1; 
				// 题目要求求种类 加过了之后就不用再加了
				// 如果题目要求的是数量 就不用赋值-1
				v = tr[v].fail;
			}
		}
		return ans;
	}

完整模板代码

luoguP3808

	#include<iostream>
	#include<queue>
	#include<string>
	#include<cstring>
	using namespace std;
	const int MAX =1000000 + 10;
	int n, cnt;
	string tmp;
	struct node{
		int fail,num;
		int ch[26];
	}tr[MAX];
	
	void build() { // 这里是建立字典树
		int len = tmp.length();
		int u = 0;
		for(int i = 0; i < len; ++i) {
			int s = tmp[i] - 'a';
			if(tr[u].ch[s] == 0)
				tr[u].ch[s] = ++cnt;
			u = tr[u].ch[s];
		}
		tr[u].num++;
	}
	void get_fail() { // 这里是fail指针 用链表串起来
		queue<int> q;
		for(int i = 0; i < 26; ++i) {
			if(tr[0].ch[i]) {
				tr[tr[0].ch[i]].fail = 0;
				q.push(tr[0].ch[i]);
			}
		}
		while(!q.empty()) {
			int u = q.front();
			q.pop();
			for(int i = 0; i < 26; ++i) {
				if(tr[u].ch[i]) {
					tr[tr[u].ch[i]].fail = tr[tr[u].fail].ch[i];
					q.push(tr[u].ch[i]);
				} else {
					tr[u].ch[i] = tr[tr[u].fail].ch[i];
				}
			}
		}
	}
	int ac() {
		int len = tmp.length();
		int u = 0, ans = 0;
		for(int i = 0; i < len; ++i) {
			int s = tmp[i]-'a';
			u = tr[u].ch[s];
			int v = u;
			while(v && tr[v].num != -1) {
				ans += tr[v].num; 
				tr[v].num = -1; 
				// 题目要求求种类 加过了之后就不用再加了
				// 如果题目要求的是数量 就不用赋值-1
				v = tr[v].fail;
			}
		}
		return ans;
	}
	int main() {
		cin >> n;
		for(int i = 0; i != n; ++i) {
			cin >> tmp;
			build();
		}
		tr[0].fail = 0;
		get_fail();
		cin >> tmp;
		int ans = ac();
		cout << ans << endl;
		return 0;
	}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光—暗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值