AC算法学习笔记

1、算法流程图

(1)    void Init()

此函数是初始化函数,用来给fail数组和goto数组初始化值。

(2)    void GotoFunction(string x)

这个函数的作用是生成有限自动机状态转移图。

(3) void FailFunction(int target,int k)

这是fail函数,核心内容是求出每个状态的fail值。

(4) void UpdateOutput()

这是update输出函数。其作用是更新每个状态的输出值。

5void Check(string x)

这个是check函数,其作用是判断改状态下output函数是否有输出,如果有输出就输出相应状态下的字符串。并且决定该状态接受输入之后的去向,如果fail,则调用该状态的fail 函数来决定去向。

6int main()

主函数,整个过程的入口。




2、自动机所定义的数据结构及其功能

(1)             int Goto[M][26];

goto数组是状态机状态的载体,内部存储着本次实验的全部状态。起始状态为0,之后每获得一个有效输入就生成一个新的状态。但是在生成状态之前要进行检验,看是否已经存在本次状态。

(2)             int Fail[M];

fail数组存储的是该状态获得输入后,如果结果为fail之后的转向状态。

(3)             string Output[M];

output数组是一个字符串数组,存储的是以该状态为终结状态的字符串。当然,字符串不唯一,AC算法的核心任务之一就是找到每个状态为终结状态时候的全部输出字符串。

(4)             string Depth[M];

depth数组用来标示该状态在第几层。我们在此次实验中将goto函数创建的状态看作一个树,因此必然需要一个数组来指明树中的节点所在的层数。




3、转向函数、失效函数、输出函数的构建过程

(1)             转向函数

我们首先来看其伪代码:

结合伪代码和刚才的函数流程图,我们可以看出转向函数首先对数组进行初始化。其次,来看while循环。如果g(state,aj)!=fail,那么就将g(state,aj)赋值给state,其目的是如果已经存在的状态就不必再次创建,只需要不断地向前更新状态即可。可是如果g(state,aj)=fail,那么我们就要创建新的状态,即newstate+1,并将g(state,aj)指向此状态,再更新状态。在函数最后,构建部分output函数。

(2)             失效函数

我们来看fail函数的伪代码:

Fail函数采用队列作为核心数据结构。首先将0状态后的有效状态加入队列。如果队列不空,就会一直执行while循环中的代码。首先将队首取出,将队首能够到达的有效状态依次加入队列。求出已取出的队首的fail值并作为state。接下来判断g(state,a)是否为fail。如果不是fail,那么该值就会作为新入队列的队首的fail值。依次类推,用队列以层序的方式将状态图中每一个状态的fail值都求出来。求出了改状态的fail值之后,应该将此状态的输出并上fail状态的输出。这是很关键的一步,用以更新output数组输出值。

(3)             输出函数

同样我们来看看output函数的伪代码

Output本质就是在模拟自动机执行的过程。首先进入while循环,如果g(state,a)为fail,那么就调用改状态的fail函数,并将函数值更新给state。直到跳出while循环,之后状态往前走一步,并判断改状态是否有输出。如果有输出,就先将改状态的输出打印出来,再继续读入下一个输入。




4、  源代码

#include<iostream>

#include<string.h>

#define M 20//State_Number

using namespace std;

int Goto[M][26];

int Top;

int Fail[M];

string Output[M];

string Depth[M];

void Init()

{

Top=0;

for(int i=0;i<M;i++)

{

Fail[i]=0;

for(int j=0;j<26;j++)

{

Goto[i][j]=0;

}

Depth[i]=Output[i]="";

}

Depth[0]+=‘0‘;

}

void GotoFunction(string x)

{

int len=x.length();

int next=0;

for(int i=0;i<len;i++)

{

int index=x[i]-97;/*a->0*/

if(Goto[next][index]==0)

{

Goto[next][index]=++Top;

next=Top;

}

else

{

next=Goto[next][index];

}

char num=next+48;/*0->‘0‘*/

if(Depth[i+1].find(num)==Depth[i+1].npos)

{

/*

这段代码很巧妙,他本质上是用一个数组来模拟树

其作用是让i+1层囊括这一层的所有状态

*/

Depth[i+1]+=num;//每一层都有哪些状态

}

}

Output[next]+=x;//构建output数组,在next位置输出x字符串

}

void FailFunction(int target,int k)

{

for(int i=0;i<Depth[k].length();i++)

{

int num=Depth[k][i]-48;

for(int j=0;j<26;j++)

{

if(Goto[num][j]==target)

{

/*

这一段是核心代码

首先找到state

然后根据算法构建target的fail值

*/

int state=Fail[num];

Fail[target]=Goto[state][j];

return;

}

}

}

}

void UpdateOutput()

{

int k=2,num;

Fail[0]=0;

for(int i=0;i<Depth[1].length();i++)

{

num=Depth[1][i]-48;

Fail[num]=0;//当然啦,我们规定层数为一的状态fail函数值都为0

}

while(Depth[k]!="")

{

for(int i=0;i<Depth[k].length();i++)

{

num=Depth[k][i]-48;

FailFunction(num,k-1);

/*

这一段是核心代码

就好像广度优先遍历

对于每一层的每一个状态

构建其fail函数值

*/

if(Output[Fail[num]]!="")

{

Output[num]+=" ";

Output[num]+=Output[Fail[num]];

/*

当然这也是核心代码

重构output内部值

*/

}

}

k++;

}

for(int i=0;i<=Top;i++)

{

cout<<‘\n‘<<i<<‘\t‘<<Output[i];

}

}

void Check(string x)

{

int state=0,index,i=0;

while(i<x.length())

{

index=x[i]-97;

if(Goto[state][index]!=0||state==0)

{

/*

0状态无论输入什么都不报错

*/

state=Goto[state][index];

if(Output[state]!="")

{

cout<<i+1<<‘\t‘<<Output[state]<<‘\n‘;

}

i++;

}

else

{

state=Fail[state];

}

}

}

int main()

{

Init();

int i=1;

cout<<"welcome the AC world!"<<endl;

cout<<"please input the "<<i <<" patterns: ";

string x;

cin>>x;

while(x!="exit")

{

i++;

cout<<"please input the "<<i <<" patterns: ";

GotoFunction(x);

cin>>x;

}

UpdateOutput();

cout<<"\n\n";

cin>>x;

Check(x);

}

时间: 2024-10-21 12:59:20

AC算法学习笔记的相关文章

算法学习笔记 递归之 快速幂、斐波那契矩阵加速

递归的定义 原文地址为:http://blog.csdn.net/thisinnocence 递归和迭代是编程中最为常用的基本技巧,而且递归常常比迭代更为简洁和强大.它的定义就是:直接或间接调用自身.经典问题有:幂运算.阶乘.组合数.斐波那契数列.汉诺塔等.其算法思想: 原问题可分解子问题(必要条件): 原与分解后的子问题相似(递归方程): 分解次数有限(子问题有穷): 最终问题可直接解决(递归边界): 对于递归的应用与优化,直接递归时要预估时空复杂度,以免出现用时过长或者栈溢出.优化递归就是以

EM算法学习笔记2:深入理解

文章<EM算法学习笔记1:简介>中介绍了EM算法的主要思路和流程,我们知道EM算法通过迭代的方法,最后得到最大似然问题的一个局部最优解.本文介绍标准EM算法背后的原理. 我们有样本集X,隐变量Z,模型参数θ,注意他们3个都是向量,要求解的log似然函数是lnp(X|θ),而这个log似然函数难以求解,我们假设隐变量Z已知,发现lnp(X,Z|θ) 的最大似然容易求解. 有一天,人们发现引入任意一个关于隐变量的分布q(Z),对于这个log似然函数,存在这样一个分解: lnp(X|θ)=L(q,θ

算法学习笔记 KMP算法之 next 数组详解

最近回顾了下字符串匹配 KMP 算法,相对于朴素匹配算法,KMP算法核心改进就在于:待匹配串指针 i 不发生回溯,模式串指针 j 跳转到 next[j],即变为了 j = next[j]. 由此时间复杂度由朴素匹配的 O(m*n) 降到了 O(m+n), 其中模式串长度 m, 待匹配文本串长 n. 其中,比较难理解的地方就是 next 数组的求法.next 数组的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀,也可看作有限状态自动机的状态,而且从自动机的角度反而更容易推导一些. "前

算法学习笔记 最短路

图论中一个经典问题就是求最短路,最为基础和最为经典的算法莫过于 Dijkstra 和 Floyd 算法,一个是贪心算法,一个是动态规划,这也是算法中的两大经典代表.用一个简单图在纸上一步一步演算,也是很好理解的,理解透自己多默写几次即可记住,机试时主要的工作往往就是快速构造邻接矩阵了. 对于平时的练习,一个很厉害的 ACMer  @BenLin_BLY 说:"刷水题可以加快我们编程的速度,做经典则可以让我们触类旁通,初期如果遇见很多编不出,不妨就写伪代码,理思路,在纸上进行整体分析和一步步的演算

[算法学习笔记]直接插入排序笔记

直接插入排序概念: 带排元素放在elem[0...n-1]中,初始化时,elem[0]自成1个有序区,无序区为elem[1...n-1],从i=1起,到i=n-1,依次将elem[i]插入有序区[0...n-1]中 直接插入排序算法步骤: 1.在当前有序区域R[1,i-1]中查找R[i]的正确插入位置K(1<=K<=i-1) 2.将R[K,i-1]中的记录均向后移动 3.移动后腾出K位置,插入R[i] (最坏)时间复杂度:O(n^2) 空间复杂度:O(1) /// <summary>

八大排序算法学习笔记:冒泡排序

冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法. 它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成.这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端. 算法原理: 比较相邻的元素.如果第一个比第二个大,就交换他们两个. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对.在这一点,最后的元素应该会是最大的数. 针对所有

由LCS到编辑距离—动态规划入门—算法学习笔记

一切计算机问题,解决方法可以归结为两类:分治和封装.分治是减层,封装是加层. 动态规划问题同样可以用这种思路,分治. 它可以划分为多个子问题解决,那这样是不是用简单的递归就完成了?也许是的,但是这样会涉及太多的不便的操作.因为子问题有重叠! 针对这种子问题有重叠的情况的解决,就是提高效率的关键. 所以动态规划问题可以总结为:最优子结构和重叠子问题. 解决这个子问题的方式的关键就是:memoization,备忘录. 动态规划算法分以下4个步骤: 描述最优解的结构 递归定义最优解的值 按自底向上的方

八大排序算法学习笔记:插入排序(一)

插入排序     包括:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序).属于稳定排序的一种(通俗地讲,就是两个相等的数不会交换位置) . 直接插入排序: 1.算法的伪代码(这样便于理解):     INSERTION-SORT (A, n)             A[1 . . n] for j ←2 to n do key ← A[ j] i ← j – 1 while i > 0 and A[i] > key do A[i+1] ← A[i]

八大排序算法学习笔记:插入排序(二分插入排序)

二分插入排序   也称折半插入排序, 1.基本思想:设数列[0....n]分为两部分一部分是[0...i]为有序序列,另一部分是[i+1.....n]为无序序列,从无序序列中取一个数 x ,利用二分查找算法找到 x 在有序序列中的插入位置并插入,有序序列还是有序的,接下来重复上述步骤,直到无序序列全部插入有序序列 ,这是整个序列只剩下有序序列即有序了. 2.代码:    3.复杂度: 用二分插入排序所要进行的总比较次数为O(lgn),当n较大时,比直接插入排序的最大比较次数小得多,但大于最小比较