ps:实验要求文件的推荐 NFA 数据格式有大问题,对于NFA来说,因为NFA对于一个字符可能有多个转移状态,所以推荐的格式不能全部储存到。我做了修改,替换成了状态转移表的存储格式,但是出现了一个新的问题:对于一个状态到另一个状态如果有多个字符可以到达那么无法表示。谨慎参考。不过,状态子集用unsigned long long 二进制表示存储非常方便,推荐。
编译原理第二次实验报告
(一)NFA—>DFA(2小时)
一、实验目的
学习和掌握将NFA转为DFA的子集构造法。
二、实验任务
(1)存储NFA与DFA;
(2)编程实现子集构造法将NFA转换成DFA。
三、实验内容
(1)确定NFA与DFA的存储格式。
要求为3个以上测试NFA准备好相应有限自动机的存储文件。(可利用实验一(二)的基础)
NFA和DFA的存储格式(输入文件txt):
格式为:
自动机类型的字符串NFA或者DFA
字符表字符数量(包括空字符’’)
字符表字符(包括空字符’’)
状态数量
状态编号 开始状态标记 接收状态标记
状态编号 开始状态标记 接收状态标记
……
状态编号 开始状态标记 接收状态标记
状态编号 开始状态标记 接收状态标记
起始状态 识别字符 到达状态
起始状态 识别字符 到达状态
……
起始状态 识别字符 到达状态
起始状态 识别字符 到达状态
(2)用C或C++语言编写将NFA转换成DFA的子集构造法的程序。
NFA_Trans_DFA()子集法NFA确定化函数:
首先需要一个子集数组,类型为unsigned long long,二进制表示时位值为1表示此位置的次序状态存在此子集中。随后声明一个子集转换表move_dfa[MAXS][MAXN]二维数组,用来存储子集法NFA确定化时产生的子集字符转换关系。再将subset[0]存储开始状态的闭包,调用closure()函数,closure()函数内部又调用search()函数通过自动机的状态转换表move[MAXN][MAXN]递归搜索空字符转换边求得闭包。
之后进入大循环,大循环的终止条件是所有子集都已经在子集数组中了。下一层循环是对每一个子集遍历字符表,将每一个字符下对应的转换状态子集的闭包求出。此时需要调用charTrans()函数求得转换状态,随后在对转换状态子集调用closure()函数求得闭包。每次求得闭包后需要检查是否已经在子集数组中了,不在则扩充子集数组,并且更新子集转换表move_dfa[MAXS][MAXN]。最后根据得到的子集数组和子集转换表move_dfa[MAXS][MAXN]来更新NFA,将其转换为DFA。
(3)测试验证程序的正确性。
求出NFA与DFA的语言集合的某个子集(如长度小于某个N),再证实两个语言集合完全相同!
因为输出字符串太多,所以详情请开文档:
out_1.txt out_2.txt out_3.txt out_4.txt
屏幕输出:
(二)DFA最小化(2小时)
一、实验目的
学会编程实现等价划分法最小化DFA。
二、实验任务
先完善DFA,再最小化DFA。
三、实验内容
(1)准备3个以上测试DFA文件。(提示:其中一定要有没有最小化的DFA)
因为我编写的程序是先将NFA确定化为DFA,再将DFA最小化。所以初始的输入文件只设计了NFA,DFA是NFA确定化后运行可以在屏幕显示。
NFA和DFA的存储格式(输入文件txt):
如上图
格式为:
自动机类型的字符串NFA或者DFA
字符表字符数量(包括空字符’’)
字符表字符(包括空字符’’)
状态数量
状态编号 开始状态标记 接收状态标记
状态编号 开始状态标记 接收状态标记
……
状态编号 开始状态标记 接收状态标记
状态编号 开始状态标记 接收状态标记
起始状态 识别字符 到达状态
起始状态 识别字符 到达状态
……
起始状态 识别字符 到达状态
起始状态 识别字符 到达状态
(2)用C或C++语言编写用等价划分法最小化DFA的程序。
Minimize_DFA()等价划分法最小化DFA函数:
首先声明一个等价划分数组divide[MAXN]和最大划分数num = 1。divide[MAXN]存储次序状态属于划分的数字,num是最大的划分数字。初始划分是,非接收状态划分值为0,接收状态划分值为1。随后进入大循环进行遍历划分值开始进行划分,首先将对应划分值的划分状态次序单独拿出来存储进数组set[MAXN]中。随后遍历字符表的所有字符,以当前划分的首状态set[0]为标杆,当同划分的其他状态通过move[MAXN][MAXN]状态转换表到达的状态不与set[0]进入状态属于同一划分时,即divide数组元素的值不一样,则进行再划分。
再划分的状态的划分值为num+1,并且更新标记flag = 1,表示新的划分产生,num的值再进行自增。而且对于当前划分需要遍历完所有字符才表示完全划分干净了,并且在此过程中有的状态进入了新的划分,需要将它们踢出去,利用divide划分值识别不将他们与当前划分首状态set[0]进行比较。最后,当划分完成时,num值不在变化,循环终止。最后根据划分数组divide[MAXN][MAXN]进行更新最小化后的DFA。
(3)经测试无误。测试不易。可求出两个DFA的语言集合的某个子集(如长度小于某个N),再证实两个语言集合完全相同!
因为输出字符串太多,所以详情请开文档:
out_1.txt out_2.txt out_3.txt out_4.txt
屏幕输出:
如上图
(三)自动机类以及其他的实现函数
-
自动机类以及方法函数(注释已详细介绍)
-
void read() 读取输入文件更新自动机
](https://img-blog.csdnimg.cn/20201122122225199.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1hNUFRGUQ==,size_16,color_FFFFFF,t_70#pic_center)
这个函数的实现逻辑很简单,就是依照上面我使用的NFA、DFA存储方法来一行一行更新自动机的数据。不过需要注意的是:
if(letter[i] == '’){ //如果为空字符
pos = i; //记录空字符次序
continue;
}
这一步将NFA的空字符’’出现的次序i用pos记录下来。因为DFA字符表是没有空字符的,所以我们需要记录空字符位置以便删除。
if(pos != -1){ //如果字符表有空字符
letter[pos] = letter[letter_num-1]; //交换空字符与字符表末尾字符的次序
letter[letter_num-1] = ‘’;
char_order[letter[pos]] = pos; //重新调整字符->次序转换表
char_order[’’] = letter_num-1;
}
这部分逻辑是将字符表末尾字符与空字符交换位置,然后更新 字符—次序 转换表。这样DFA字符表删除空字符时可以直接将字符数目减一就可以了。
3.map<string,string> getString(int order,string ans,ull
curlen,map<string,string> m){ //递归查询自动机能识别的字符串
getString()函数是不断循环本状态通过状态转换表move[][]可以到达的状态进行字符串的增长获取。不过需要注意的是在NFA中当字符串长度等于最大长度length但状态不是接收状态时也不能递归返回,因为可能经过了几个空字符后到达接收状态。所以只能continue。其他情况就根据字符是否为空字符进行字符串及长度的更新递归。最后状态所有到达状态都搜索完毕时返回
4. void printString(){ //在文件中输出自动机能识别的所有长度小于等于LEN的字符串
这个函数逻辑也很简单,就是在输出文件内输出自动机的数据结构队列set存储的可识别字符串。只需要注意的是,当自动机set还未生成,为空时,需要先调用createQueue()函数生成。
5.void createQueue(){ //生成自动机能能识别的所有长度小于等于LEN的字符串队列
通过对自动机每一个开始状态调用之前解析的getString()函数获得所有长度小于等于length的可识别字符串,先存储在map中进行去重去空字符。随后遍历map容器将字符串存入自动机队列set中,以字典序顺序。
6. vector getOrder(ull set){ //获取子集中所有状态的次序
通过状态的unsigned long long 类型二进制表示来得知子集中包含的状态的次序。不断将子集数右移,然后将最低位与0x1进行位与,如果为1则此位置值1,如果为0则此位置值0。再由不断自增一的sum来获得次序,最后压入vector中。
7. ull search(ull ans,int num,bool flag[]){ //递归搜索子集通过不限次空字符获得的子集
search()函数通过递归不断获取一个子集的状态如何通过不限次的空字符得到另一个子集。就是由当前状态经过状态转换表的空字符move[][]获得下一状态,最后将状态的二进制表示与子集位或获得最终的子集。不过需要注意的是要用flag[]数组来标记经过的状态,不然会进入死循环当中。
8. ull closure(ull ans){ //获取子集闭包closure
closure()函数通过调用getOrder()获得子集包含的状态次序并存储到vector中后,对每一个状态调用search()函数递归搜索空字符串到达状态,最后形成的子集就是闭包。
9. ull charTrans(ull set,char ch){ //获取子集通过字符转换形成的子集
charTrans()函数调用getOrder()获得子集状态次序后,对每一个状态经过字符ch达到的状态存入子集tmp中,最后返回tmp。
10. int inSet(ull subset[],int size,ull set){ //检测子集是否在子集集合中并返回次序
inSet()函数通过循环if分支判断子集set是否在子集集合subset中,当存在时返回子集set在子集集合subset中的次序,否则返回-1。
11. void isEqual(AUTOMATA automata[]){ //由识别字符串集合判断 NFA DFA min_DFA 是否等价
isEqual()通过比较两个自动机的可识别字符串队列set是否相同来判定两个自动机是否等价。利用一个双重循环对三个自动机进行两两比较,首先当可识别字符串队列set的大小不同时,将对应标识flag置0,否则继续利用循环判断两者队列的字符串是否相同(可识别字符串在队列中已经是字典序排列了)。如果有一个可识别字符串不同,那么也将对应标识flag置0。最后根据三个标记flag[3]的值来判断三个自动机之间的等价情况。输出提示信息。
需要注意的是,一些情况需要排除,三者中有两对相互等价,但是剩下的一对不等价的情况是不可能的,比如a b c中a == b, a == c, 那么必定b == c。
12.int main() //主函数
因为需要调用isEqual()函数判断NFA DFA min_DFA三者的等价性,所以需要把它们声明成一个长度为3的AUTOMATA类数组。
(四)完整源代码
#include <bits/stdc++.h>
#define ull unsigned long long
#define MAXN 500
#define MAXS 100
#define LEN 7 //识别输出字符串最大长度
#define IN "in_1.txt" //输入文件 "in_1.txt" "in_2.txt" "in_3.txt" "in_4.txt"
#define OUT "out_1.txt" //输出文件 "out_1.txt" "out_2.txt" "out_3.txt" "out_4.txt"
using namespace std;
class AUTOMATA{
//自动机类
public:
string type; //自动机类型:NFA DFA
bool is_min; //是否最小化的标志
ull length; //设置最大识别字符串长度
int letter_num; //字符表长度
char letter[MAXN]; //字符表数组
int state_num; //状态集合数目
struct State{
//储存状态信息的结构体
int id; //状态id
ull bin_id; //在状态集合的二进制次序
bool is_start,is_finish; //是否为开始、接收状态的标志
State(){
//结构体初始化构造函数
id = 0; bin_id = 0;
is_start = 0; is_finish = 0;
}
}state[MAXN]; //状态数组
int start_num; //开始状态数目
int start_state[MAXN]; //开始状态在状态集合次序的集合
int fin_num; //接收状态数目
int fin_state[MAXN]; //接收状态在状态集合次序的集合
map<char,int> char_order; //字符->字符表次序 转换表
map<int,int> id_order; //状态id->状态数组次序 转换表
queue<string> set; //自动机能识别的长度小于等于length的字符串去重去空字符串集合
char move[MAXN][MAXN]; //自动机状态转换表的二维数组
AUTOMATA(); //自动机初始化构造函数
void read(); //读入文件数据更新自动机
void showAutomata(); //打印输出自动机的全部信息
map<string,string> getString(int,string,ull,map<string,string>); //递归查询自动机能识别的字符串
void printString(); //在文件中输出自动机能识别的所有长度小于等于LEN的字符串
void createQueue(); //生成自动机能能识别的所有长度小于等于LEN的字符串队列
bool legalDetect(); //自动机合法性的检查
int inSet(ull*,int,ull); //检测子集是否在子集集合中并返回次序
vector<int> getOrder(ull); //获取子集中所有状态的次序
ull charTrans(ull,char); //获取子集通过字符转换形成的子集
ull search(ull,int,bool*); //递归搜索子集通过不限次空字符获得的子集
ull closure(ull); //获取子集闭包closure
AUTOMATA NFA_Trans_DFA(); //NFA确定化函数
AUTOMATA Minimize_DFA(); //DFA最小化函数
};
AUTOMATA:: AUTOMATA(){
//自动机初始化构造函数
length = LEN;
is_min = letter_num = state_num = start_num = fin_num = 0;
memset(letter, 0, sizeof(letter));
memset(start_state, -1, sizeof(start_state));
memset(fin_state, -1, sizeof(fin_state));
memset(move, 0, sizeof(move));
}
void AUTOMATA:: read(){
//读入文件数据更新自动机
int pos = -1; //NFA空字符检测标志
ifstream infile(IN,ios::in);
assert(infile.is_open());
getline(infile,type); //更新自动机类型
infile >> letter_num; //更新字符表长度
for(int i = 0; i < letter_num; i++){
//更新字符表
infile >> letter[i];
if(letter[i] == '_'){
//如果为空字符
pos = i; //记录空字符次序
continue;
}
char_order[letter[i]] = i; //更新字符->次序转换表
}
if(pos != -1){
//如果字符表有空字符
letter[pos] = letter[letter_num-1]; //交换空字符与字符表末尾字符的次序
letter[letter_num-1] = '_';
char_order[letter[pos]] = pos; //重新调整字符->次序转换表
char_order['_'] = letter_num-1;
}
infile >> state_num; //更新状态集合数目
for(int i = 0; i < state_num; i++){
//更新状态数组
infile >> state[i].id;
state[i].bin_id = 1 << i; //无符号long long变量的二进制表示在状态数组次序置 1,其余位置 0
infile >> state[i].is_start >> state[i].is_finish;
if(state[i].is_start)
start_state[start_num++] = i;
if(state[i].is_finish)
fin_state[fin_num++] = i;
id_order[state[i].id] = i; //更新状态id->次序转换表
}
while(!infile.eof()){
int id1,id2;
char ch;
infile >> id1 >> ch >> id2;
move[id_order[id1]][id_order[id2]] = ch; //更新自动机状态转换表的二维数组
}
infile.close