词法分析程序之NFA转DFA和DFA的最小化

原理(1)——NFA转DFA

转化方法
我们必须知道如何将DFA转化为DFA——利用子集构造法
步骤:

  • 为NFA添加一个开始结点和终止结点(上一节的程序本来就是这样设计)
  • 对NFA中的结点,对开始结点做闭包,得到结点集合,以该结点集合开始,计算该集合对每一种符号的Ia、Ib
  • 观察这些Ia、Ib在第一列是否存在?存在则不需要添加,否则添加,然后对其计算Ia、Ib,重复操作,直到所有的都已经添加过了。
  • 对上述的结点集合重新命名,得到确定化的DFA的状态转化表。
  • 根据该表选出代表结点,连接边,得到确定化的DFA图。

原理不懂的可以去看编译原理黄贤英的书或者参考如下:
https://blog.csdn.net/qq_40294512/article/details/89004777

关于闭包运算

  • 对结点集合I,如果该集合为单结点,其经过若干 空 弧路径得到的结点集合称为I的闭包:Eclosure(I)
  • 对结点集合I,其每个结点经过1条 a 弧路径(允许经过空,只要保证a弧数目为1)得到的结点集合称为J:Eclosure_A(I),对该集合J做闭包得到的Ia = Eclosure(J)则为I的a弧转换。

编写的算法实现:

/*
 * 计算nfa某个节点的闭包,返回vector<State>  nfa的节点集合
 *
*/
vector<State> EclosureI(State nfaState,NFACell& cell){
    vector<State> I(1);
    I[0] = nfaState;
    return EclosureJ(I,cell);

}
vector<State> EclosureJ(vector<State>& J,NFACell& cell){
    map<State,bool> result2;
    if(isVisit.size()<=0){
        getNFAStateNum(cell);//节点数组初始化
    }
    for(size_t i=0;i<J.size();i++){
        dfsTraver(result2,J[i],'#',cell);//#表示空
        resetIsVisit();//重置isvist,使得所有的边都未访问过
    }
    //将节点集合转化为数组
    vector<State> Ia = map2vector(result2);
    return Ia;
}
/*
 * 以某个nfa的节点开始,遍历整个nfa,得到某个符号的 集合J(经过若干 transymbol到达的节点)
*/
void dfsTraver(map<State,bool> &result,State& nfaState,char transymbol,NFACell& cell){
//    map<State,bool>::const_iterator iterator = nodeSet.begin();
//    for(;iterator!=nodeSet.end();++iterator){
//        isVisit[iterator->first] = false;//标记为未访问
//    }
//开始dfs
       if(result.count(nfaState)==0){//第一个要加入
          result.insert(map<State,bool>::value_type(nfaState,true));
          dfs(result,nfaState,transymbol,cell);
       }

//只需要遍历一个节点
}
/*
 * 因为我们的nfa结构是类似边集数组,所以用dfs有点难
 * 直接遍历所有的边,但是isvisit的下标和实际边不对应,如果利用查找,复杂
 * -------------我们把isvisit(之前是节点数组的访问控制)改为对边的访问进行控制
*/
void dfs(map<State,bool> &result,State& nfaState,char transymbol,NFACell& cell){
    for(int j=0;j<cell.EdgeCount;j++){
      //  如果该边没被访问过且其转化符号为transymbol则加入
            Edge e = cell.EdgeSet[j];
            if((e.StartState.StateName.compare(nfaState.StateName)==0) && (!isVisit[j]) && e.TransSymbol==transymbol){//相同节点不可能满足最后一个条件
                //所以我们只考虑不同节点
                if(result.count(e.EndState)==0){
                    result.insert(map<State,bool>::value_type(e.EndState,true));
                }
                 isVisit[j] = true;//该边标记为访问过
                // cout<<(e.StartState.StateName)<<"-->";
                 dfs(result,e.EndState,transymbol,cell);//然后该边的终止节点成为新的开始节点递归下去
            }
    }
}
*
 * J =  move(I,a),从集合I出发,每个节点经过一条transymbol(这里使用a)弧得到的状态集合(注意,这里可以经历很多条 空E 路径,
 * 只要保证a的路径个数为1
*/
vector<State> moveA(vector<State> I,NFACell& cell,char transymbol){
    //必须保证isvisit已经赋值
    if(isVisit.size()<=0){
        getNFAStateNum(cell);
    }else{
        resetIsVisit();//重置isvist,使得所有的边都未访问过
    }
    map<State,bool> result1;
    char temp = transymbol;
    int transNum = 0;
    for(size_t i=0;i<I.size()&&I[i].StateName!="";i++){
        move(result1,I[i],cell,transymbol,transNum);
        transymbol = temp;//重置计算下一个节点
        transNum = 0;//重置计算下一个节点
        resetIsVisit();//重置isvist,使得所有的边都未访问过
    }
    //将节点集合转化为数组
    vector<State> vertexes = map2vector(result1);
    return vertexes;

}
/**
 * @brief move
 * @param result
 * @param I
 * @param cell
 * @param transymbol
 * @param transNum
 * @return 空
 * -----------------只给moveA调用
 */
void move( map<State,bool> &result,State I,NFACell& cell,char &transymbol,int &transNum){
    //对状态I计算经过一条a弧线的集合,返回给它。
    for(int j=0;j<cell.EdgeCount;j++){
        //  如果该边没被访问过且其转化符号为transymbol则加入
        Edge e = cell.EdgeSet[j];
        if((e.StartState.StateName.compare(I.StateName)==0) && (!isVisit[j]) && (e.TransSymbol==transymbol||e.TransSymbol=='#')){
            //所以我们只考虑不同节点
            if(e.TransSymbol == transymbol && transymbol!='#'){
                transNum++;//来到这里,说明已经有一条a弧线了,我们接下来只能找‘#’的弧线了
                transymbol = '#';
            }
            if(transNum==1){
                if(result.count(e.EndState)==0){//不存在则添加
                    result.insert(map<State,bool>::value_type(e.EndState,true));
                }
            }

            isVisit[j] = true;//该边标记为访问过
            //  cout<<(e.StartState.StateName)<<"-->";
            move(result,e.EndState,cell,transymbol,transNum);//然后该边的终止节点成为新的开始节点递归下去
        }
    }
    transNum = 0;//这里,因为我们递归一个节点结束,对a弧的计算清0
}

DFA需要什么数据结构
我们还需要像思考NFA一样,明确DFA应该使用什么数据结构?以下是我的思考过程:

  • 需要的数据结构
    存储状态转化表中的第一列I——DFA的所有状态,使用hashmap<Integer,dfaState>
    Integer理解为下标,也可以将其按照某个逻辑映射为String
  • 存储状态转化表的除第一列以外的列——DFA的该状态的转化关系,使用hashmap<Integer,dafaState> 一对多。
  • 不同列的转化符号不同,需要额外添加数据结构记录——全局符号表
  • 有一个数据结构来记录终态集合,将其表示域置为true——分为终态和非终态,为后面的DFA化简打基础
  • 状态信息表,存储全局DFA的状态符号,它与Integer下标存在映射

DFA实际数据结构设计

  • 节点——节点符号,是否为终态节点,在hashmap中的映射key
  • 边——边的起始节点,终止节点,转化符号
  • 整个DFA:开始节点、终止节点集合、边集合、边数
struct DFAStateInfo{
    string stateFlag2[26*2] = {
        "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z",
        "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"
    };
    int stateNum;//记录当前DFA的状态已经使用了多少个
};

struct dfaNode{
    string stateName;//状态信息
    vector<State> nfaCell;//包含的nfa节点集合
    bool endNodeFlag;//是否为终态节点
    //int key;//key = atoi(stateName)//映射
    bool operator<(const dfaNode &state) const{//根据源码,如果a==b,则返回0
        if((this->stateName.compare(state.stateName))!=0){
            return this->stateName<state.stateName;
        };//如果相等则输出为0,不等则输出为-1。//不能添加 this->stateName == state.stateName;
        return 0;
    }
};
struct dfaEdge{
    dfaNode startNode;
    dfaNode endNode;
    char transymbol;//转化符号
};
struct DFACell{
    dfaNode startNode;//开始节点信息
    vector<dfaNode> endNodeSet;//终态节点
    dfaEdge edgeSet[MAX];//边
    int edgeCount;//边数
    vector<dfaNode> allNode;//所有的节点信息
};

原理(2)——DFA最小化

使用分割法,取出代表结点。

/**
 * @brief minimizeDFA
 * @param dfa
 * @return
 * 最小化DFA的含义:
 *          没有多余状态(死状态,无法到达的状态)——消除多余状态的方法是删除
 *          没有两个状态是互相等价的(不可区别)
 *                  需要满足两个条件:1、一致性,同是终态或者同是非终态
 *                                  2、传播性,对于所有的输入符号,状态s和t都必须转化到等价的状态里
 *
 * 如何最小化?
 *  第一步、划分终态和非终态  假设目前划分{D},{A,B,C}
 *  第二步、对每个子集考察是否可以再划分——每个节点可以到达的状态是否一致
 *                  对每个符号表的符号,计算moveA(j,Transymbol)判断得到的集合是否都属于j,是则继续判断其他符号
 *                  如果 有某个j集合中的元素(B)得到的集合状态不被j所兼容,即有某个状态(D)不在J中,则说明该元素和集合j中其他元素可区分。
 *                  如果存在多个元素都不兼容,将他们划分到一起,
 *          得到了划分后的更多的子集,回退第二步,继续划分没有访问过的子集(使用一个队列控制即可)

 * 第三步、然后{A,C}中取出一个代表节点A,所有关于C的连线由A代理。
 * 第四步、完成边的修改、节点的更新、完成最小化
 *
 * 含有原来初态的子集还是初态,含有终态的子集还是终态
 *
 *
 * 也就是说:
 *       我们需要一个判断兼容性的函数 isin(dfanode,vector J); 判断节点dfanode是否属于J
 *          我们还需要一个对dfanode节点集合 重命名的的函数
 *
 *
 *
 *
 *
 */
DFACell minimizeDFA(DFACell& dfa){
    cout<<"开始最小化DFA"<<endl;
    //划分终态和非终态集合
    cout<<"划分终态和非终态集合"<<endl;
    vector<dfaNode> endSet = dfa.endNodeSet;
    vector<dfaNode> startSet;
    for(size_t i=0;i<dfa.allNode.size();i++){
        if(dfa.allNode[i].endNodeFlag==false){
            startSet.push_back(dfa.allNode[i]);
        }
    }
    vector<vector<dfaNode>> stateAllSet;//用于存储最终不可划分到状态集合
    //对子集考察是否可以再划分
    cout<<"对子集考察是否可以再划分"<<endl;
    queue<vector<dfaNode>> listSet;//队列判断,每一次划分只会分出终态和非终态两个子集,记录未考察的子集
    if(startSet.size()>1){
        listSet.push(startSet);
    }else if(startSet.size()==1){
        stateAllSet.push_back(startSet);
    }
    if(endSet.size()>1){
        listSet.push(endSet);
    }else if(endSet.size()==1){
        stateAllSet.push_back(endSet);
    }
//    int time=0;//测试用

    while(!listSet.empty()){
 //       cout<<"考察"<<time++<<endl;
        vector<dfaNode> temp = listSet.front();
        listSet.pop();
        vector<dfaNode> newSet1;//创建一个变量存储新的分割出的子集
        vector<dfaNode> newSet2;//创建一个变量存储新的分割出的子集
 //状态Si和Sj对于任意输入符a∈Σ,必须转到等价的状态里,否则Si和Sj是可区别的。
        //newSet2.push_back(temp[0]);//以第一个节点为基准,所以它在newset2
       for(size_t i=0;i<symbolTable.size();i++){//对每一种符号
           //以第一个节点经过符号t到达的节点状态为划分基准 判断 他们的等价性
           dfaNode object = moveA(temp[0],dfa,symbolTable[i]);
           for(size_t j=1;j<temp.size() && temp[j].stateName.compare("#")!=0;j++){
                //计算从 temp[j] 节点经过一条 transymbol得到的dfa节点
                dfaNode a_Node = moveA(temp[j],dfa,symbolTable[i]);
                if(a_Node.stateName.compare(object.stateName)!=0){//不属于原来的子集
                    newSet1.push_back(temp[j]);
                    //将该节点置空
                    temp[j].stateName = "#";//表示从集合temp里面去除了它了,因为他已经被划分到newset1里面了
                }
            }
        }
        //经过上面的划分,还存在temp中的则等价
        for(size_t j=0;j<temp.size() && temp[j].stateName.compare("#")!=0;j++)
            newSet2.push_back(temp[j]);
        if(newSet1.size()>1){//大于1才具有可划分特性
            listSet.push(newSet1);
        }else if(newSet1.size()==1){//必须存在节点,才能加入最终的节点集合
            stateAllSet.push_back(newSet1);
        }
        if(newSet2.size()>=1)//必须存在节点,才能加入最终的节点集合
            stateAllSet.push_back(newSet2);//每次划分都能得到一个结果,它的size必然大于等于1

    }

    //对节点集合进行取出代表节点和更新边的关系:
    cout<<"对节点集合进行取出代表节点和更新边的关系"<<endl;
    DFACell newDfa;
    newDfa.edgeCount=0;
    getUpdateEdgeDFA(stateAllSet,dfa,newDfa);
    cout<<"完成最小化!"<<endl;
    displayDFA(newDfa);
    return newDfa;
}

具体的细节实现还有很多很多,自己从0写出来,我是觉得有点难的,毕竟我写了3、4天才写出来一个,但是可能鲁棒性不是很高,我只测试了自己的数据,可能存在很多bug,但是毕竟精力不够了。先这样吧。

测试部分

话不多说,测试数据:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码结构

在这里插入图片描述
在这里插入图片描述

在看的点个赞支持一下呀!

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雨夜※繁华

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

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

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

打赏作者

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

抵扣说明:

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

余额充值