【C++实现】编译原理 免考小队 NFA转换为等价的DFA

背景

期末考试免考,冲!

实验名称

对任意给定的NFA M进行确定化操作

实验时间

2020年5月21日 到 2020年5月24日

院系

信息科学与工程学院

组员姓名

Chocolate、kry2025、钟先生、leo、小光

实验环境介绍

  • windows 10 操作系统
  • Eclipse 进行 java 编程
  • CodeBlocks 进行 C++ 编程

实验目的与要求

目的

  • 深刻理解 NFA 确定化操作
  • 掌握子集构造算法过程
  • 加强团队合作能力
  • 提高自身的编程能力和解决问题的能力

要求

NFA 转换为等价的 DFA

正则 到 NFA的转换

有穷自动机

作用:将输入的序列转换成一个状态图,方便之后的处理。通常被用在词法分析器中。
1)有穷自动机是一个识别器,对每个可能的的输入串简单的回答“是”或“否”。
2)有穷自动机分为两类:
a)不确定的有穷自动机(NFA)对其边上的标号没有任何限制。一个符号标记离开同一状态的多条变,并且空串ε也可以作为标号。
b)确定的有穷自动机(DFA)有且只有一条离开该状态,以该符号位标号的边。

不确定的有穷自动机

正则式(RE)转不确定型有穷自动机(NFA)


找出所有可以被匹配的字符即符号集合∑作为每一列的字段名,然后从起始态开始
1)状态0可以匹配a,匹配后可以到状态0或状态1,记为∅。匹配b只能得到状态0,记为{0}。
2)状态1可以匹配a,没有匹配到,记为∅。匹配b得到状态2,记为{2}。
3)状态0可以匹配a,没有匹配到,记为∅。匹配b得到状态3,记为{3}。
4)状态0可以匹配a,没有匹配到,记为∅。匹配b没有匹配到,记为∅。

至此,状态表建立完成。正则式(RE)转不确定型有穷自动机(NFA)完成。

NFA 到 DFA

NFA M 确定化

1)根据NFA构造DFA状态转换矩阵:

  • ①确定DFA的字母表,初态(NFA的所有初态集)
  • ②从初态出发,经字母表到达的状态集看成一个新状态
  • ③将新状态添加到DFA状态集
  • ④重复②③步骤,直到没有新的DFA状态

2)画出DFA

3)看NFA和DFA识别的符号串是否一致

NFA N 确定化

涉及操作:

算法:

更多详细内容可参考:NFA到DFA的转换及DFA的简化

实验过程

对NFA M 确定化

采用二进制方法来对NFA M 进行确定化,先来说说其中用到的关键知识:

将状态集合用二进制表示,例如,如果一个集合(从0开始)包含了 0,1,2,总共有4个状态(即 0,1,2,3 ),那么,当前集合用 0111 表示。同理,如果包含了0,3 ,总共有4个状态(即 0,1,2,3 ),那么,当前集合用 1001 表示。

x & (-x) 表示拿到 x 的最右边第一个

a & x 可以判断 a 状态集合 是否包含 x 状态集合

a ^= x 可以用来将 x 状态集合 加入到 a 状态集合

下面依次来讲述代码实现:

int ans[maxn],one[maxn],zero[maxn],lft[maxn],rgt[maxn];
char change[maxn];
bool vis[maxn],ac[maxn];

用到的数据结构:

  • ans 数组表示所求的状态集合
  • one 数组表示状态经 1 所到达的状态
  • zero 数组表示状态经 0 所到达的状态
  • lft 数组表示经过 0 状态到达的状态集合
  • rgt 数组表示经过 1 状态到达的状态集合
  • change 数组用来转换输出结果,即将状态集合用字母 ‘A’-‘Z’ 来表示
  • vis 数组用来去重操作,判断当前状态集合是否存在过
  • ac 数组用来判断集合状态中是否包含终态,若包含,置为1

下面函数是用来找到对应状态下标

//找到对应的状态下标
int index(int p){
    int x = 1;
    if(p == 1)  //p为1表示当前为初始状态
        return 0;
    int i = 0;
    while(++i){  //循环找出当前对应的状态下标
        x <<= 1;
        if(p == x)
            return i; //找到即返回对应下标
    }
    return 0;
}

move操作

int moveT(int a, int b){
    while(b){
        int x = b&(-b);  //去当前集合中的最后一个节点
        if(!(a&x))   //如果不存在该节点,加入集合当中
            a ^= x;
        b ^= x;  //已经存在该节点,就进行舍去操作
    }
    return a;
}

核心代码,将状态集合逐个拿出来,进行 move 操作,然后进行去重操作,最后进行更新新的状态集合

void dfs(int p){
    ans[cnt] = p;
    int lsum = 0, rsum = 0;
    while(p){
        int x = p&(-p);  //取出当前集合中的最后一个节点
        int y = index(x); //找到对应的状态下标
        lsum = moveT(lsum, zero[y]); //进行move操作
        rsum = moveT(rsum, one[y]);  //进行move操作
        p ^= x;   //将当前拿出来的节点从原集合中去掉
    }
    lft[cnt] = lsum;  //更新当前的状态集合
    rgt[cnt] = rsum;  //更新当前的状态集合
    cnt++;            //更新状态行数
    if(!vis[lsum])
        vis[lsum] = 1, dfs(lsum);  //进行去重操作
    if(!vis[rsum])
        vis[rsum] = 1, dfs(rsum); //进行去重操作
}

输入处理

while(cin>>preNode){
    if(preNode=='$') break;
    cin>>tchar>>nexNode;
    if(tchar-'a'==0) zero[preNode-'0']|=(1<<(nexNode-'0'));
    else one[preNode-'0']|=(1<<(nexNode-'0'));
}

输出处理

for(int i=0;i<cnt;i++)
    change[ans[i]]=i+'A';  //输出处理,用字母'A'-'Z'来表示集合

对NFA N 确定化

用到的数据结构:

struct edge{
    char preNode;  //前驱节点
    char tchar;    //弧
    char nexNode;  //后继节点
}e[maxn];
//获得的状态集合
struct newJ{
    string setJ;
};
//集合与集合之间的转换关系
struct relation{
    newJ* preJ;
    char jchar;
    newJ* nexJ;
};
  • edge 结构体用来表示边的信息
  • newJ 表示状态集合J
  • relation 结构体表示最后输出的转换关系表

求某个状态的闭包

//得到闭包
void getEClosure(const edge* e,int cntEdge,newJ* st){
    for(int i=0;i<st->setJ.length();i++){
        for(int j=0;j<cntEdge;j++){  //遍历所有的边
            if(st->setJ[i] == e[j].preNode && e[j].tchar=='#')
                st->setJ+=e[j].nexNode;
        }
    }
}

move操作

//move操作
void moveT(char ttchar,const edge* e,int cntEdge,newJ* source,newJ* dest){
    //e为所有边的集合,然后就能从一个转换字符得到全部的,比如2得到bd,而不会第一个2得到b,第二个2得到d
    for(int i=0;i<source->setJ.length();i++){
        for(int j=0;j<cntEdge;j++){ //遍历所有的边
            if(source->setJ[i] == e[j].preNode && e[j].tchar == ttchar)
                dest->setJ+=e[j].nexNode;
        }
    }
}

去重操作,判断是否加入 allSet(总状态集合) 中

//通过状态集合中的setJ来决定是否添加
bool isInsert(vector<newJ*> allSet,newJ* newSet){
    for(int i=0;i<allSet.size();i++){
        if(allSet.at(i)->setJ == newSet->setJ)
            return false;
    }
    return true;
}

去重操作,判断当前状态转换是否加入转换关系表中

//判断relation结构体去重
bool isInsertForRel(vector<relation*> relVec,newJ* preJ,char jchar,newJ* nexJ){
    for(int i=0;i<relVec.size();i++){
        if(relVec.at(i)->preJ->setJ == preJ->setJ && relVec.at(i)->jchar == jchar && relVec.at(i)->nexJ->setJ == nexJ->setJ)
            return false;
    }
    return true;
}

重命名转换函数

//重命名转换函数
void changeName(vector<newJ*> allSet,newJ* newSet,string& newStr){
    newJ* tmpJ = new newJ();
    for(int i=0;i<allSet.size();i++){
        if(allSet.at(i)->setJ == newSet->setJ)
            tmpJ->setJ = 'A'+i;
    }
    newStr = tmpJ->setJ;
}

后续省略…(详情请见后文源代码说明)

实验结果(附源代码)

对 NFA M 确定化

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn=999999;
int ans[maxn],one[maxn],zero[maxn],lft[maxn],rgt[maxn];
char change[maxn];
bool vis[maxn],ac[maxn];
int cnt,n,q,f;
//找到对应的状态下标
int index(int p){
    int x = 1;
    if(p == 1)  //p为1表示当前为初始状态
        return 0;
    int i = 0;
    while(++i){  //循环找出当前对应的状态下标
        x <<= 1;
        if(p == x)
            return i; //找到即返回对应下标
    }
    return 0;
}
int moveT(int a, int b){
    while(b){
        int x = b&(-b);  //去当前集合中的最后一个节点
        if(!(a&x))   //如果不存在该节点,加入集合当中
            a ^= x;
        b ^= x;  //已经存在该节点,就进行舍去操作
    }
    return a;
}
void dfs(int p){
    ans[cnt] = p;
    int lsum = 0, rsum = 0;
    while(p){
        int x = p&(-p);  //取出当前集合中的最后一个节点
        int y = index(x); //找到对应的状态下标
        lsum = moveT(lsum, zero[y]); //进行move操作
        rsum = moveT(rsum, one[y]);  //进行move操作
        p ^= x;   //将当前拿出来的节点从原集合中去掉
    }
    lft[cnt] = lsum;  //更新当前的状态集合
    rgt[cnt] = rsum;  //更新当前的状态集合
    cnt++;            //更新状态行数
    if(!vis[lsum])
        vis[lsum] = 1, dfs(lsum);  //进行重复操作
    if(!vis[rsum])
        vis[rsum] = 1, dfs(rsum); //进行重复操作
}
int main(){
    int t;
    cout<<"多组输入,请先输入对应的组数:"<<endl;
    cin>>t;  //多组输入
    while(t--){
        cout << "输入各边的信息,并且以 '前点(char '0'-'1000')   转换字符(a 或 b)   后点(int '0'-'1000')'格式,结束以'$'开头" << endl;
        char preNode,tchar,nexNode;
        while(cin>>preNode){
            if(preNode=='$') break;
            cin>>tchar>>nexNode;
            if(tchar-'a'==0) zero[preNode-'0']|=(1<<(nexNode-'0'));
            else one[preNode-'0']|=(1<<(nexNode-'0'));
        }
        q=1;
        cout<<"输入终止状态集合,结束以'$'开头"<<endl;
        char endNode;
        while(cin>>endNode){
            if(endNode=='$') break;
            f|=(1<<(endNode-'0'));
        }
        cnt=0;
        memset(vis,0,sizeof(vis)); //初始化
        memset(ac,0,sizeof(ac)); //初始化
        vis[q]=1;
        dfs(q);    //转换开始
        int sum=0;
        for(int i=0;i<cnt;i++)
            if(ans[i]&f)   //判断所求集合中是否包含终态
                ac[i]=1,sum++;  //标记终态集合并统计个数
        for(int i=0;i<cnt;i++)
            change[ans[i]]=i+'A';  //输出处理,用字母'A'-'Z'来表示集合
        cout<<"转换结果:"<<endl;
        cout<<"DFA的状态数:"<<cnt<<" "<<"终止状态数:"<<sum<<endl<<endl;
        cout<<"终态:"<<endl;   //输出终态集合
        for(int i=0,j=0;i<cnt;i++){
            if(ac[i]){
                if(j)
                    cout<<" ";
                cout<<(char)(i+'A');
                j++;
            }
        }
        cout<<endl<<endl; //输出DFA状态转换矩阵
        cout<<"由NFA得到的DFA状态转换矩阵:"<<endl;
        cout<<"----------------------------"<<endl;
        cout<<"  "<<"a"<<" "<<"b"<<endl;
        cout<<"----------------------------"<<endl;
        for(int i=0;i<cnt;i++)  //输出打印新的转换结果
            cout<<(char)('A'+i)<<" "<<change[lft[i]]<<" "<<change[rgt[i]]<<endl;
        cout<<"----------------------------"<<endl;
        cout<<endl;
    }
    return 0;
}

输出结果

输出结果文字版

多组输入,请先输入对应的组数:
100
输入各边的信息,并且以 '前点(char '0'-'1000')   转换字符(a 或 b)   后点(int '0'-'1000')'格式,结束以'$'开头
0 b 2
4 a 0
0 a 1
1 a 1
2 b 3
3 b 2
3 a 3
5 a 5
4 b 5
5 b 4
1 b 4
2 a 1
$
输入终止状态集合,结束以'$'开头
0
$
转换结果:
DFA的状态数:6 终止状态数:1

终态:
A

由NFA得到的DFA状态转换矩阵:
----------------------------
  a b
----------------------------
A B E
B B C
C A D
D D C
E B F
F F E
----------------------------

对NFA N 确定化

#include<bits/stdc++.h>
using namespace std;
const int maxn=1000;
struct edge{
    char preNode;  //前驱节点
    char tchar;    //弧
    char nexNode;  //后继节点
}e[maxn];
//获得的状态集合
struct newJ{
    string setJ;
};
//集合与集合之间的转换关系
struct relation{
    newJ* preJ;
    char jchar;
    newJ* nexJ;
};
//得到闭包
void getEClosure(const edge* e,int cntEdge,newJ* st){
    for(int i=0;i<st->setJ.length();i++){
        for(int j=0;j<cntEdge;j++){  //遍历所有的边
            if(st->setJ[i] == e[j].preNode && e[j].tchar=='#')
                st->setJ+=e[j].nexNode;
        }
    }
}
//move操作
void moveT(char ttchar,const edge* e,int cntEdge,newJ* source,newJ* dest){
    //e为所有边的集合,然后就能从一个转换字符得到全部的,比如2得到bd,而不会第一个2得到b,第二个2得到d
    for(int i=0;i<source->setJ.length();i++){
        for(int j=0;j<cntEdge;j++){ //遍历所有的边
            if(source->setJ[i] == e[j].preNode && e[j].tchar == ttchar)
                dest->setJ+=e[j].nexNode;
        }
    }
}
//通过状态集合中的setJ来决定是否添加
bool isInsert(vector<newJ*> allSet,newJ* newSet){
    for(int i=0;i<allSet.size();i++){
        if(allSet.at(i)->setJ == newSet->setJ)
            return false;
    }
    return true;
}
//判断relation结构体去重
bool isInsertForRel(vector<relation*> relVec,newJ* preJ,char jchar,newJ* nexJ){
    for(int i=0;i<relVec.size();i++){
        if(relVec.at(i)->preJ->setJ == preJ->setJ && relVec.at(i)->jchar == jchar && relVec.at(i)->nexJ->setJ == nexJ->setJ)
            return false;
    }
    return true;
}
//重命名转换函数
void changeName(vector<newJ*> allSet,newJ* newSet,string& newStr){
    newJ* tmpJ = new newJ();
    for(int i=0;i<allSet.size();i++){
        if(allSet.at(i)->setJ == newSet->setJ)
            tmpJ->setJ = 'A'+i;
    }
    newStr = tmpJ->setJ;
}
int main(){
    int cntEdge=0; //统计边的数量
    char staNode;  //初始状态点
    string endNode,node; //终止状态节点和总的节点集合
    int cntRealEdge[maxn]={0}; //用于判断是否包含空字符的边 为1代表含空字符的边 初始话为0,不包含
    cout << "输入各边的信息,并且以 '前点(char '0'-'1000')   转换字符(char 'a'-'z')   后点(char '0'-'1000')'格式,结束以'$'开头" << endl;
    cout << "如果转换字符为空,则用'#'表示" << endl;
    char ttchar[2];
    for(int i=0;i<maxn;i++){
        cin>>e[i].preNode; //输入前点
        if(e[i].preNode == '$') break; //遇到'$' 结束输入
        cin>>ttchar;
        cin>>e[i].nexNode; //输入转换字符及后点
        e[i].tchar=ttchar[0];
        if(ttchar[0] == '#') cntRealEdge[cntEdge]=1; //标记含有空字符的边
        ++cntEdge; //统计边的数量
    }
    //将输入的边节点进行整合
    for(int i=0;i<cntEdge;i++){
        char preTmp = e[i].preNode;
        char nexTmp = e[i].nexNode;
        if(node.find(preTmp)>node.length()) node+=preTmp;
        if(node.find(nexTmp)>node.length()) node+=nexTmp;
    }
    cout<<"输入初始点字符:"<<endl;
    cin>>staNode;
    while(node.find(staNode)==string::npos){
        cout<<"初始状态输入错误,请重新输入:"<<endl;
        cin>>staNode;   //错误即重新输入一次
    }
    cout<<"输入终止点字符(若有多个终结状态,直接写成字符串形式)"<<endl;
    cin>>endNode;
    bool inputStatus=true; //用于结束终止点字符的输入
    while(inputStatus){
        for(int i=0;i<endNode.length();i++){
            if(node.find(endNode[i])==string::npos){
                cout << "终结状态输入错误,请重新输入:" << endl;
                cin >> endNode;
            }
        }
        inputStatus=false;
    }
    newJ* newSet = new newJ();
    newSet->setJ = staNode; //设置初始点为状态集合I
    getEClosure(e, cntEdge, newSet); //得到闭包
    vector<newJ*>allSet(1, newSet);  //设置所有状态集合的向量
    /*用来存储每一次的闭包操作前的第一列的状态集合
    *比如第一次strVec存储的是初始状态,求闭包时多了2个状态集合。在第二次时存储的是新的2个状态,原先的初始状态被去除。
    *总的状态集合存储在allSet中
    */
    vector<newJ*>strVec(1, newSet);
    int sizeOfStrVec = 1;   //初始大小就是初始点所构成的状态集合
    vector<relation*>relVec; //转换关系表的向量
    while(sizeOfStrVec){ //如果不符合则说明新增的集合都是原有的集合
        int oldAllSize=allSet.size();  //求出目前存储的状态集合大小
        for(int j=0;j<sizeOfStrVec;j++){
            for(int i=0;i<cntEdge;i++){
                newJ* dest=new newJ();
                if(!cntRealEdge[i]){  //不是空字符边的,对它进行move操作
                    moveT(e[i].tchar, e, cntEdge, strVec.at(j), dest);
                    //如果有一个字符在多条边上,所以要按字符相同的归类集合。否则就会使得状态集合分开而造成错误!!
                    getEClosure(e, cntEdge, dest); //此时dest为 Ia,Ib之类的
                    if(isInsert(allSet,dest) && dest->setJ!="") //没找到并且dest->setJ且不为空则添加
                        allSet.push_back(dest);
                    //在添加relVec时,只要是不为空就要添加,这里会使relDest的元素可能重复(当一个字符出现在多条边中)
                    if (dest->setJ != ""){
                        relation* relDest = new relation();
                        relDest->preJ = strVec.at(j);
                        relDest->jchar = e[i].tchar;
                        relDest->nexJ = dest;
                        bool isIN = isInsertForRel(relVec,relDest->preJ,relDest->jchar,relDest->nexJ); //去重
                        if(isIN) relVec.push_back(relDest);
                    }
                }
            }
        }
        strVec.clear();  //去除原状态集合
        for(int i=oldAllSize;i<allSet.size();i++){//将allSet中新增的后面元素添加进strVec中
            newJ* dest = new newJ();
            dest = allSet.at(i);
            strVec.push_back(dest);
        }
        sizeOfStrVec = strVec.size(); //求出目前存储的状态集合大小
    }
    cout << "转换结果:" << endl;
    vector<relation*>::iterator relIt;
    for(relIt=relVec.begin();relIt!=relVec.end();relIt++) //遍历输出关系转换表的集合
        cout<<(*relIt)->preJ->setJ<<" "<<(*relIt)->jchar<<" "<<(*relIt)->nexJ->setJ<<endl;
    char upperChars[26];
    memset(upperChars,0,sizeof(upperChars));
    cout<<"重命名如下:"<<endl;
    for(int i=0;i<allSet.size();i++){
        upperChars[i] = 'A'+i; //通过下标将每一个集合依次从'A'开始 命名
        cout<<upperChars[i]<<":"<<allSet.at(i)->setJ<<endl;
    }
    vector<relation*> newRelVec; //重命名后的relVec;
    for(int i=0;i<relVec.size();i++){
        relation* newRel = new relation();  //依次更换新的名称
        string preNew,nexNew;
        changeName(allSet,relVec.at(i)->preJ,preNew);
        changeName(allSet,relVec.at(i)->nexJ,nexNew);
        newJ* tpreJ = new newJ();
        newJ* tnexJ = new newJ();
        newRel->preJ = tpreJ;
        newRel->nexJ = tnexJ;
        newRel->preJ->setJ = preNew;
        newRel->nexJ->setJ = nexNew;
        newRel->jchar = relVec.at(i)->jchar;
        newRelVec.push_back(newRel);
    }
    //输出验证重命名的集合关系
    cout << "最终转换:" << endl;
    vector<relation*>::iterator newRelIt;
    for(newRelIt = newRelVec.begin();newRelIt!=newRelVec.end();newRelIt++)  //遍历输出新的关系转换表的集合
        cout<<(*newRelIt)->preJ->setJ<<" "<<(*newRelIt)->jchar<<" "<<(*newRelIt)->nexJ->setJ<<endl;
    //输出终止状态
    cout<<"终态:"<<endl;
    set<char> st;  //通过set来进行去重操作
    set<char>::iterator it;
    for(int k=0;k<allSet.size();k++){
        for(int i=0;i<endNode.size();i++){
            if(allSet.at(k)->setJ.find(endNode[i]) != string::npos)  //确保终态在集合中存在
                st.insert('A'+k);   //放入set集合中,达到去重效果
        }
    }
    for(it=st.begin();it!=st.end();it++) cout<<(*it)<<" "; //遍历输出终态集合
    cout<<endl;
    return 0;
}

输出结果

输出结果文字版

输入各边的信息,并且以 '前点(char '0'-'1000')   转换字符(char 'a'-'z')   后点(char '0'-'1000')'格式,结束以'$'开头
如果转换字符为空,则用'#'表示
a 1 b
b 1 b
b 2 b
b # e
a # c
c 1 c
c 2 c
c 1 d
$
输入初始点字符:
a
输入终止点字符(若有多个终结状态,直接写成字符串形式)
ed
转换结果:
ac 1 bcde
ac 2 c
bcde 1 bcde
bcde 2 bce
c 1 cd
c 2 c
bce 1 bcde
bce 2 bce
cd 1 cd
cd 2 c
重命名如下:
A:ac
B:bcde
C:c
D:bce
E:cd
最终转换:
A 1 B
A 2 C
B 1 B
B 2 D
C 1 E
C 2 C
D 1 B
D 2 D
E 1 E
E 2 C
终态:
B D E

参考文献

感谢以下博主的文章,本文参考了部分代码和知识。

Hungryof:NFA到DFA的转换

zhen12321:编译原理-(NFA->DFA)

发芽ing的小啊呜:【20200319】编译原理课程课业打卡九之NFA的确定化

Just-Live:湖大OJ-实验C----NFA转换为DFA

爱玩游戏的小隐:正则到NFA的转换

爱玩游戏的小隐:NFA到DFA的转换及DFA的简化

学如逆水行舟,不进则退
一百个Chocolate CSDN认证博客专家 CSDN博客专家 Vue爱好者 博客之星
不是只会写业务代码的前端开发攻城狮!博客网站:yangchaoyi.vip做限量版的自己,就这样安静地努力。一个还在苦学前端的小小Chocolate,我的博客主要分享前端、算法、大学课程笔记、平常遇到的bug、心得感悟体会,感谢您的访问,若喜欢可以关注一下~每一个清晨,记得鼓励自己。没有奇迹,只有你努力的轨迹;没有运气,只有你坚持的勇气!每一份坚持都是成功的累积,只要相信自己,总会遇到惊喜!座右铭:学如逆水行舟,不进则退!
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值