• 【学时总结&模板时间】◆学时·10 & 模板·3◆ AC自动机


    ◇学时·10 & 模板·3◇ AC自动机

    跟着高中上课……讲AC自动机的扩展运用。然而连KMP、trie字典树都不怎么会用的我一脸懵逼<(_ _)>

    花一上午自学了一下AC自动机 QwQ


    ◦ Trie树

    字典树的一种(听说还有其他字典树,不清楚)。每个节点代表一个字母,根节点相当于超级源点,根节点不表示字母。Trie树最大的特点是从根节点出发,沿着树边向下走,走过的节点会形成一个字符串。而一些节点是某一个单词的结尾,对于这种节点,我们一般会给它做一个标记(ovr)。

    ▪ 构建Trie树(Build)

    根据Trie树的特点,最初的树是一个空集,只包含根节点。当我们要向树中插入一个单词str时,从根节点出发,如果根节点有表示str[0]的儿子,则移步到该儿子;否则新建立一个表示str[0]的儿子,再移步。以此类推,当我们要插入str[k]时,我们应该在第(k+1)层的某一个节点now(根节点为第一层),如果节点now有表示str[k]的儿子,则移步,否则先创建表示str[k]的儿子,再移步。直到将整个单词遍历完才结束。假设我们结束时的节点在now,那么ovr可以做两种基本的标记:① 该节点是多少个单词的结尾;② 该节点是哪一个单词的结尾……当然如果题目有一些奇怪的要求的话可以用ovr存储一些奇怪的东西,甚至多定义几个ovr也可以。

    void Build(string str,int id){
    	int len=str.length(),now=0; //当前节点是now,trie[0]是根节点
    	for(int i=0;i<len;i++){
    		if(!trie[now].son[str[i]-'a'])
    			trie[now].son[str[i]-'a']=++cnt; //cnt类似于指针,用于新建节点,(cnt+1)指向最近的一个空节点
    		now=trie[now].son[str[i]-'a']; //移步
    	}
    	trie[now].ovr=id; //做标记,这里是存储的trie[now]是哪一个单词的结尾
    }
    

    ▪ 与AC自动机的关系

    AC自动机是建立在Trie树上的,只是围绕KMP的fail函数增添了一些边。


    ◦ KMP

    一种字符串匹配算法,在朴素的字符串匹配算法的基础上进行了可观的优化。若要在字符串A里查找字符串B,则称A为“主串”,B为“模式串”,当我们尝试一次匹配时发现匹配失败,则称为“失配”。

    匹配时有两个指针,i表示从主串的第i个位置开始,j表示模式串匹配到了第j个位置。当朴素算法在主串第i个位置失配时,j会回到0,而i就+1,即从主串下一个位置继续从模式串的第一个位置开始匹配,这样会造成一种浪费——下一次匹配并没有利用到之前失配的匹配的已经匹配好的信息。

    而KMP算法对其进行了优化。

    ▪ KMP算法的原理

    KMP算法认为“不需要将模式串一个位置一个位置地向右滑动”,例如:

    当模式串"abca"在主串"abcd"失配后,我们没有必要将i++,因为主串的下一个位置不是'a',逐步滑动不一定会匹配。而KMP算法就会在发现失配后,直接将主串向右移动到可能匹配的最远位置!

    当模式串的某一个前缀是模式串的真子串时,我们在失配后可以直接将模式串移动到该位置。

    (不知道怎么解释了,看上面的3张图片吧)

    ▪ Fail函数

    为了实现主串失配时指针不回溯,只调整模式串指针j,使模式串向右尽可能远地滑动,定义失配函数Fail(j),表示当模式串中第j个字符与主串中Si失配时,在模式串中可能和主串中Si匹配的字符的位置。

    转移式则是:fail[i]=①-1(i=0);②max{ k|0<k<j, 且p0 …pk-1=pj-k+1 …pj-1 };③0(其他情况)。


    ◦ AC自动机

    ▪ 插入单词和Trie树是一样的( ̄▽ ̄)"

    ▪ 节点的结束单词统计也和Trie树是一样的

    ▪ 获取Fail函数

    这里是用BFS获取的。当单词在字典树的第二层就失配即在第一个字符就失配时,fail一定是0。也就是说第二层节点的fail都指向根节点。我们将第一层的所有节点都push进队列里,然后如果节点u本来有"a"+i儿子v,则将v的fail指向u的fail的"a"+i儿子,否则直接将v指向u的fail的"a"+i儿子。

    void GetFail(){
    	queue< int > que;
    	for(int i=0;i<26;i++) //遍历第二层
    		if(trie[0].son[i])
    			trie[trie[0].son[i]].fail=0,
    			que.push(trie[0].son[i]);
    	while(!que.empty()){
    		int u=que.front();que.pop();
    		for(int i=0;i<26;i++) //找儿子节点
    			if(trie[u].son[i]){ //有表示"a"+i的儿子
    				trie[trie[u].son[i]].fail=trie[trie[u].fail].son[i];
    				//指向父亲的fail的"a"+i儿子
    				que.push(trie[u].son[i]);
    			}
    			else
    				trie[u].son[i]=trie[trie[u].fail].son[i];
    				//直接将儿子指向父亲fail的"a"+i儿子
    	}
    }

    ▪ 主串上的递推

    设now是当前所处的节点。从根节点开始则now的初始值为0。从头到尾枚举主串字符str[i],先将now赋值为now的str[i]儿子。再沿着now的fail指针一直回溯到根节点,可以实现遍历str[0~i]的每一个后缀。对于str的每一个前缀都求出全部后缀,就相当于求出了str的全部子串。

    根据题目要求统计答案。

    void ACQuery(string str){
    	int len=str.length();
    	int now=0;
    	for(int i=0;i<len;i++){
    		now=trie[now].son[str[i]-'a']; //移动now
    		for(int j=now;j;j=trie[j].fail) //按fail指针回溯
    			ans[trie[j].ovr].num++; //统计答案
    	}
    }
    

    The End

    Thanks for reading!

    - Lucky_Glass

    (Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)
  • 相关阅读:
    「学习笔记」杂项算法学习笔记
    「CF484E」Sign on Fence「整体二分」「线段树」
    「BZOJ 2653」middle「主席树」「二分」
    「BZOJ 5010」「FJOI 2017」矩阵填数「状压DP」
    jmeter阶梯式加压测试
    jmeter监控内存,CPU等方法
    jmeter 读取多个用户名并同时发
    APP性能测试工具
    Android--iOS抓取崩溃日志
    安装并使用PICT,生成测试用例
  • 原文地址:https://www.cnblogs.com/LuckyGlass-blog/p/9829574.html
Copyright © 2020-2023  润新知