背景
期末考试免考,冲!
实验名称
对任意给定的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 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
参考文献
感谢以下博主的文章,本文参考了部分代码和知识。
zhen12321:编译原理-(NFA->DFA)
发芽ing的小啊呜:【20200319】编译原理课程课业打卡九之NFA的确定化
Just-Live:湖大OJ-实验C----NFA转换为DFA
爱玩游戏的小隐:正则到NFA的转换
学如逆水行舟,不进则退