DFA的化简(Java描述)
【问题描述】
实现把 DFA 最小化的算法
【基本要求】
1、输入一个 DFA,注意:状态转换矩阵的表示是关键。
2、化简该 DFA。
3、输出化简后的 DFA 的五元组。
【数据结构】
1、如何表示DFA?
DFA的五元组表示为:{状态集合、字母表、状态转换矩阵、开始状态和终止状态集合}。
状态集合、字母表和终止状态集合均可以使用字符数组进行存储。开始状态可以使用字符存储也可以存储初态在状态集合(数组)中的下标,这里存储初态在状态集合中对应的下标。
(默认初态唯一,不唯一就加一个S使初态唯一)
DFA的核心是其状态转换矩阵,这里用一个二维数组表示,状态转换矩阵数组的行下标对应状态集合中下标相同的状态,状态转换矩阵数组的列下标对应字母表中下标相同的字母。
如上图描述的是一个状态集合为{S,A,B,C,D,E,F},字母表为{a,b}的DFA状态转换矩阵。已知B对应的a弧度转换为A;B在状态集合数组中的下标为2,a在字母表数组中的下标为0,所以状态转换矩阵中stateChange[0][2]存储的就是B的a弧转换结果A
可以依此构建DFA类:
public class DFA {
int start; //初态序号
char[] letterList; //字母表
char[][] stateChange; //状态转换矩阵
char[] statue; //状态集合
char[] endStatue; //终止状态集合
}
2、如何对DFA状态集合进行划分?
既然是对状态集合进行操作,那么自然想到使用集合HashSet。
在状态集合未划分之前,可以将其看作一个数组,看作一个整体。一旦集合开始划分,就会产生多个状态集合。将相互等价的状态放在同一个集合中,用此集合的第一个状态字符代替表示。对划分出的多个状态集合再存储在一个大的集合里。
【细节问题分析】
1、如何输入一个DFA?
DFA使用五元组表示,此处控制台输入一行字符串由五部分组成:状态集合、字母表、状态转换矩阵、开始状态和终止状态集合,之间由‘ ,’隔开。其中状态转换矩阵按行下标递增依次输入。其间不允许出现空格和换行。同时要求输入必须是合法的DFA,这里就把判断DFA是否合法交给了输入者。
例如:状态集合为{S,A,B,C,D,E,F}、字母表为{a,b}、状态转换矩阵为{ACACFFCBBDEDDE}(如上图)、开始状态S和终止状态集合为{CEDF}的DFA输入为:
SABCDEF,ab,ACACFFCBBDEDDE,S,CEDF
2、如何对输入的DFA进行格式化?
在DFA的构造方法中对DFA对象进行初始化。
控制台获取到一个字符串,要求输入者严格按照五元组顺序输入合法的DFA信息,将这个字符串按‘ , ’拆分成字符串数组,得到的五个字符串数组分别对应DFA的五元组。
此过程中值得注意的是,在创建DFA对象时调用其构造方法,此时确定此DFA状态转换矩阵的大小。DFA状态转换矩阵由一个二维数组保存,这个二维数组的行数就是DFA字母表的长度,列数就是DFA状态集合的长度。
//构造方法,并进行初始化
DFA(String s){
String[] ss = s.split(",");
//对DFA进行初始化赋值
char[] c1 = ss[0].toCharArray();
char[] startChar = ss[3].toCharArray();
for(int i=0;i<c1.length;i++){
if(c1[i]==startChar[0])
start = i; //初态序号
}
letterList = ss[1].toCharArray(); //DFA字母表
statue = ss[0].toCharArray(); //DFA状态集合
endStatue = ss[4].toCharArray(); //DFA终止状态集合
stateChange = new char[letterList.length][statue.length]; //确定矩阵大小
char[] SC = ss[2].toCharArray();
for(int i=0;i<letterList.length;i++){
for(int j=0;j<statue.length;j++){
//System.out.println("i="+i+" j="+j);
//System.out.println("SC="+SC[i*(statue.length)+j]);
stateChange[i][j] = SC[i*(statue.length)+j];
}
}
}
在DFA类中还定义了对DFA格式化输出的方法:
//格式化输出DFA
public void printOut(){
System.out.print("{\n状态集合:");
for(char c : statue)
System.out.print(c+" ");
System.out.print("\n字母表:");
for(char c : letterList)
System.out.print(c+" ");
System.out.println("\n状态转换矩阵:");
for(int i=0;i<letterList.length;i++){
for(int j=0;j<statue.length;j++){
System.out.print(stateChange[i][j]+" ");
}
System.out.println();
}
System.out.println("开始状态:"+statue[start]);
System.out.print("终止状态集合:");
for(char c : endStatue)
System.out.print(c+" ");
System.out.println("\n}");
}
【核心算法—DFA化简】
有了上面DFA类和相关方法的定义,我们可以在主类中使用下面代码创建一个dfa对象,并输出加以验证。
Scanner scanner = new Scanner(System.in);
String s=null;
//控制台一次只允许读入一行DFA相关信息
if(scanner.hasNext()){
s = scanner.nextLine();
}
//初始化并创建DFA对象
DFA dfa = new DFA(s);
//打印DFA
dfa.printOut();
/*
*测试用例
*SABCDEF,ab,ACACFFCBBDEDDE,S,CEDF
*/
下面我们用分割法对DFA进行化简。
首先我们对要用到的集合进行定义:定义newSet对DFA状态集合进行存储,存储的是多个HashSet集合。定义newSet2是在对newSet进行更新时要用到。
对DFA进行化简的过程就是遍历newSet中的每个子集合中的状态字符,若此状态经过某转换达到的状态与它当前所在的状态集合不等价,则对此状态集合进行拆分。拆分条件即是某转换(如a弧转换)达到状态与当前是否等价,即newSet中的每个状态集合拆分结果为两个集合,所以定义set和set1进行临时存储。
HashSet<HashSet> newSet = new HashSet<>();
HashSet<HashSet> newSet2 = new HashSet<>(); //对newSet进行更新时预存数据
HashSet set = new HashSet();
HashSet set1 = new HashSet();
首先,将DFA的状态集合划分为两个集合:终态集合、非终态集合。
for(char c : dfa.endStatue) //构建结束符集合
set.add(c);
newSet.add(new HashSet(set));
for(char c :dfa.statue){ //构建非结束符集合
if(set.add(c)==true){
set1.add(c);
}
}
newSet.add(new HashSet(set1));
set.clear();
set1.clear();
得到DFA的终止状态集合和非终止状态集合后,可以对其进行化简了。
先给出化简过程的伪代码:
for(循环字母表){
while(判断当前状态集合是否划分彻底){
for(遍历newSet){
获取newSet中子集合的迭代器 i //Iterator i = h.iterator();
char oncee = ' '; //保存当前状态字符对应的转换结果
while(循环每个子集合){
获取当前状态字符statue //char statue = (char) i.next();
将每一个状态子集合的第一个状态字符加入set,并获取其转换结果赋值给oncee
比较此状态子集合的其他状态字符的转换结果
if(与oncee在一个集合)
将此状态字符加入set
else
将此状态字符加入set1
}
将set和set1加入newSet2;
清空set和set1;
}
去除newSet2中的空集合;
将newSet2赋给newSet; //某弧转换分割后对newSet进行更新
清空newSet2;
}
}
下面是化简过程的详细代码:
for(char l : dfa.letterList){
//判断当前状态集合是否划分彻底
while(DFA.EndJudge(newSet, dfa, l)==false){
for(HashSet h : newSet){
System.out.println("是否可再分 = "+DFA.EndJudge(newSet, dfa, l));
Iterator i = h.iterator();
int once = 1;
char oncee = ' ';
while(i.hasNext()){
char statue = (char) i.next();
if(once==1){
set.add(statue);
once=0;
//oncee = statue;
//比较对象应该是转换的目的态,即是state的l转换
oncee = dfa.stateChange[dfa.getLetterNo(l)][dfa.getStatueNo(statue)];
}
else{
System.out.println("oncee="+oncee+" toState="+dfa.stateChange[dfa.getLetterNo(l)][dfa.getStatueNo(statue)]+" methods.inSameSet="+methods.inSameSet(newSet, oncee, dfa.stateChange[dfa.getLetterNo(l)][dfa.getStatueNo(statue)])+"\n");
if(methods.inSameSet(newSet, oncee, dfa.stateChange[dfa.getLetterNo(l)][dfa.getStatueNo(statue)])==true)
set.add(statue);
else
set1.add(statue);
}
}
once=1;
newSet2.add(new HashSet(set));
newSet2.add(new HashSet(set1));
set.clear();
set1.clear();
}
//去除newSet2中的空集合
Iterator<HashSet> iteration = newSet2.iterator();
while(iteration.hasNext()){
if(iteration.next().isEmpty()){
newSet2.remove(iteration);
}
}
newSet = new HashSet<>(newSet2); //某弧转换分割后对newSet进行更新
newSet2.clear();
}
}
至此,对DFA的化简结束,下面我们的任务就是生成化简后的DFA。
【新DFA生成】
生成化简后的DFA依然根据DFA五元组分为五部分进行。
1、生成状态集合
遍历化简后的newSet集合,取每个子集合的首个状态字符代表这个集合,构成新DFA的状态集合。
StringBuffer StatueSet = new StringBuffer(""); //化简后的DFA的状态集合
for(HashSet h : newSet){
Iterator i = h.iterator();
int l=0;
while(i.hasNext()){
if(l==0){
StatueSet.append((char)i.next()+"");
l=1;
}
if(i.hasNext())
i.next();
}
//测试输出
//System.out.println(StatueSet);
}
2、字母表不变
StringBuffer LetterLis = new StringBuffer("");
for(char c : dfa.letterList){
LetterLis.append(c);
}
3、生成状态转换矩阵:核心步骤。此处同样使用一个字符串表示状态转换矩阵。
遍历已生成的新DFA的状态集合,同时遍历字母表,依次获取状态字符对应的各个转换结果(注意获取顺序按字母表顺序),初步生成状态转换矩阵。
因为先前生成状态集合时,对含有多个状态字符的集合用其首字符代替了,所以生成的状态转换矩阵中有部份状态字符是目前状态集合中不存在的。需要对转换矩阵进行修正。
StringBuffer StatueChangeSet = new StringBuffer(""); //化简后的状态转换矩阵
char[] st = StatueSet.toString().toCharArray();
//----------生成状态转换矩阵-------------------
for(int i=0;i<dfa.letterList.length;i++){
for(int j=0;j<st.length;j++){
char toStatue = dfa.stateChange[i][dfa.getStatueNo(st[j])]; //转换到的状态
StatueChangeSet.append(toStatue+"");
}
}
//----------纠正状态转换矩阵-------------------
//对在同一个集合中的状态进行合并,用集合第一个字符表示
char[] st1 = StatueChangeSet.toString().toCharArray();
for(int k=0;k<st1.length;k++){
for(HashSet h : newSet){
Iterator i = h.iterator();
while(i.hasNext()){
if(st1[k]==(char)i.next())
st1[k] = methods.getFirtEle(h);
}
}
}
StatueChangeSet = new StringBuffer("");
for(char c : st1){
StatueChangeSet.append(c);
}
//输出测试
//System.out.println(StatueChangeSet); //化简后的最终状态转换矩阵
4、开始状态不变
char Start = dfa.statue[dfa.start]; //化简后开始符不变
5、生成终止状态集合
在化简后的DFA的状态集合中寻找存在于原DFA终止状态集合中的状态,即就是化简后DFA的终止状态集合。
遍历newSet中的每个子集合,若一个子集合中含有原DFA的终止状态符,则将这个子集合判定为终止状态集合,将其首字符加入新DFA的终止状态集合中。
StringBuffer EndStatueChangeSet = new StringBuffer(""); //化简后终止状态集合
char[] st2 = dfa.endStatue.toString().toCharArray();
for(HashSet h : newSet){
Iterator i = h.iterator();
int k=1; //若确定一个集合含有终态,则不再判断此集合的其他字符
while(i.hasNext() && k==1){
char cc = (char)i.next();
for(char c : st2){
if(c==cc){
EndStatueChangeSet.append(methods.getFirtEle(h));
k=0;
}
}
}
k=1;
}
//输出测试
//System.out.println("----------\n终态集合:"+EndStatueChangeSet+"\n----------");
最后将得到的五个字符串进行处理连接成一个包含一个完成DFA信息的字符串,构造化简后的DFA对象,并打印。
String newDFA = StatueSet.toString()+','+LetterLis.toString()+','+StatueChangeSet.toString()+','+Start+','+EndStatueChangeSet.toString();
DFA smiplerDFA = new DFA(newDFA);
//System.out.println("\n--------------------------\n化简后的DFA为:");
smiplerDFA.printOut();
【相关方法说明】
在对DFA进行化简的过程中,为了使程序更加简洁清晰,将部分方法加以封装,下面进行说明。
一、DFA类中的方法
这些方法是DFA类所有的成员方法,非静态,使用DFA对象进行调用。构造方法与DFA格式化输出方法上面已经讲过,这里不再赘述。
1、获取指定状态字符在状态集合数组中的下标索引
public int getStatueNo(char c){
for(int i=0;i<statue.length;i++){
if(statue[i]==c)
return i;
}
return -1;
}
2、获取指定转换字母在字母表数组中的下标索引
public int getLetterNo(char c){
for(int i=0;i<letterList.length;i++){
if(letterList[i]==c)
return i;
}
return -1;
}
3、判断一组状态集合是否划分彻底(判断依据一组集合的某转换是否可继续分割集合)
/*
* 判断某部分集合的l转换是否不可再分
* 输入:状态集合 DFA对象 判断的字母
*
* turn false : 状态集合未划分彻底,可再分
* turn true : 状态集合不可再分
*/
public static boolean EndJudge(HashSet<HashSet> hash,DFA dfa,char l){
int judgeSymbol = 0;
int s = 0;
//char C = ' ';
char O = ' ';
for(HashSet h : hash){
Iterator i = h.iterator();
while(i.hasNext()){
char ch = (char)i.next();
if(s==0){
//C = ch;
s=1;
O = dfa.stateChange[dfa.getLetterNo(l)][dfa.getStatueNo(ch)];
}
else{
if(methods.inSameSet(hash, O, dfa.stateChange[dfa.getLetterNo(l)][dfa.getStatueNo(ch)])==false)
return false; //此集合仍可再继续划分
}
}
s=0;
}
return true;
}
二、写在methods类中的通用方法
1、获取集合的首字符
//获取集合第一个字符
public static char getFirtEle(HashSet h){
Iterator i = h.iterator();
char c = '0';
while(i.hasNext()){
c = (char)i.next();
break;
}
return c;
}
2、判断两个字符是否在一组集合中的同一个集合里
//判断两个字符是否在一个集合中
//输入参数:若干个集合,字符1,字符2
public static boolean inSameSet(HashSet<HashSet> hash,char A,char B){
if(A==B)
return true;
else{
int a=0,b=0;
for(HashSet h : hash){
Iterator i = h.iterator();
while(i.hasNext()){
char charr = (char)i.next();
if(charr==A){
a=1;
}
else if(charr==B){
b=1;
}
}
if(a+b==2)
return true;
a=0;
b=0;
}
return false;
}
}
写在最后:
由于本人水平有限,数据结构设计及算法实现不可避免会有漏洞或不足之处。欢迎大家指出思路和代码中的不足,也期待大佬分享更简洁精巧的代码实现。
欢迎转载!(注明出处 嘻嘻)