原理(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,但是毕竟精力不够了。先这样吧。
测试部分
话不多说,测试数据:
代码结构
在看的点个赞支持一下呀!