有穷自动机

 

 

定义一个状态的集合Q,而一个状态q通过一个转移函数δ则可转移到另外一个状态q', 而自动有一个初始状态,还会有一个接受状态,到达了接受状态则说明该一系列的输入时符号该自动机所确定的模式。在编译原理的课上,接触了有穷自动机,而正则表达式和有穷自动自动机是等价的,程序设计语言的词法则可以用正则表达式来定义。可见,有穷自动机用来做字符匹配那是再合适不过了。

 

 

 

一个简单的有穷自动机
  点击查看原图
如图所示,0为起始状态,1为接受状态(结束状态),箭头表示状态转移。可以略微地推导一下,aba,aaa,abba都能符合该模式,当然对于这个比较简单的例子,我们可以发现只要是包含a的字符串都能符合该有穷自动机。

 

 

 

当然要编程实现一个专门匹配某个字符串的有穷自动机,还得对模式字符串进行分析。我们的有穷状态有若干个状态,从状态0到状态n。如果将状态比作是一条大路的话,那么就有各种小路在各种状态中转换,例如《算法导论》里面有如下的插图:
  点击查看原图
可以这样看,当输入后状态转移向右,则越来越接近接受状态,然而有些转移向后,则前面的工作功亏一篑,需要辗转再回去(这就好比辛辛苦苦三十年,一朝贬到老家前)。这就是状态转换的真谛,永远在这里面徘徊转换,在这个确定性有穷自动机里面,只有一条出路,那就是7。咋一看好像和字符串匹配没有多大关系,实则字符串的匹配也是一些状态,当与当前的字符不匹配时就退回到某个状态再做打算,继续让后面的字符串做匹配,当一路向前匹配的时候,则将达到接受状态。

 

 

 

1、 后缀函数σ

 

σ(x) 是x的后缀P的最长前缀的长度。定义式为σ(x) = max{k:Pk 是 x的后缀,长度为k}, 例如P = ab, 则σ(ccab) = 2,而(cdca) = 1,如图所示。
  点击查看原图
上面是x,而下面则是P。可见是用P的前缀去做x的后缀,所得的最长的匹配长度即为k。而变迁函数说明有多少个匹配了则说明到了状态几。也就是说 δ(q,a) = σ(Pq a) ,表示在状态q的时候输入字符a,那么即等于字符串Pq(已经有q位匹配)加上一个a计算得到的最长前缀长度。(数学符号一行,胜过千言万语啊!)一切的模型仍在于一一字符比对当中,用P的前缀去做x的后缀。终态函数φ(Ti) = σ(Ti),即后缀函数的值就是Ti作为输入的终止状态值。

 

 

 

下面是程序,分为两部分,一部分从头到尾扫描字符串,另外一部分是计算变迁转移表的函数,状态变迁以二维数组的形式存储。先输入两个字符,然后输入字符集,以$结束,然后可以求出结果,编程细节上可能还有漏洞。
 
#include <string>
#include <vector>
#include <iostream>
using namespace std;
 
void cinContextAndParten(string &context, string &parten);
 
 
bool IsRearFix(string x, string y);
void ComputeTransition(string parten, vector<char> charset, int *tranTable[]);
void DFAMatch(string context, int *tranTable[], string parten, vector<char> charset);
 
int _tmain(int argc, _TCHAR* argv[])
{    
    string context;  // 内容
    string parten;    // 要匹配的模式
         
    char ch;
    cinContextAndParten(context, parten);  // 输入两个字符串
 
    cout << "请输入字符集:" << endl;
    vector<char> charset;
    while( cin>>ch && ch != '$')
    {
        charset.push_back(ch);                         // 放入字符集中
    }
    
    int charsetLen = charset.end() - charset.begin();    // 字符集大小
    int partenLen = parten.length();                // 用于构建转移函数
    int **tranTable = new int*[partenLen+1];            // 这个就是状态转换表包括一个状态
    for(int i=0; i<partenLen+1; i++)
    {
        tranTable[i] = new int[charsetLen];  
    }
    
    // 先计算出状态转换表
    ComputeTransition(parten, charset, tranTable);
    // 然后进行字符串匹配
    DFAMatch(context, tranTable, parten, charset);
 
    system("pause");
    return 0;
}
 
// 输入两个字符串
void cinContextAndParten(string &context, string &parten)
{
    std::cout << "please input context : \n";
    std::cin >> context;
    std::cout << "please input parten : \n";
    std::cin >> parten;
}
//---------------------------------- 有穷自动机--------------------------------------------//
// 传入参数目标字符串,转移函数信息,模式
void DFAMatch(string context, int *tranTable[], string parten, vector<char> charset)
{
    int n = context.length(); // 获取context长度
    int m = parten.length();
    if( n < m)                     // 先检查长短
        return;
    int q = 0;                    // 转移状态
    int k = 0;                    // k 字符context[i] 在字符集中下标
    for(int i= 0; i<n; i++)
    {
        vector<char>::iterator it;            // 迭代器
        for(it = charset.begin(); it != charset.end(); it++)
        {
            if( *it == context[i])
            {
                k = it - charset.begin();    // 下标
                break;
            }
        }
        q = tranTable[q][k];            // 从状态转移表中进行查找下一个状态
        if (q == m)                        // 为接受状态
            cout << i - m+1<< endl;        // 输出匹配位置
    }
}
 
// 根据当前模式和字母表,计算出状态转换表
void ComputeTransition(string parten, vector<char> charset, int *tranTable[])
{
    int  m = parten.length();
    for(int q=0; q<m+1; q++)  // m+1个状态
    {
        vector<char>::iterator pch;
        for(pch = charset.begin(); pch!= charset.end(); pch++)
        {
            int k = min(m+1, q+2);
            string tmp = parten.substr(0,q);            // 字符串连接
            tmp.push_back(*pch);                    // 往尾部添加一个字符
            do
            {
                k--;                                                                       // 此处显示出了KMP的玄妙,KMP将减少一些不必要的检测
            }while(! IsRearFix( parten.substr(0,k), tmp));    // 判断Pk是否是Pqa 的后缀
            // 给状态转移表赋值
            tranTable[q][pch-charset.begin()] = k; // 获得状态转移函数q 遇到字符ch 到k
        }
    }

}

点击查看原图

 

by bibodeng       2012-10-23     http://bibodeng.web-149.com