编译原理实验(化简DFA算法)

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;
		}
	}




写在最后:

由于本人水平有限,数据结构设计及算法实现不可避免会有漏洞或不足之处。欢迎大家指出思路和代码中的不足,也期待大佬分享更简洁精巧的代码实现。

欢迎转载!(注明出处 嘻嘻)

  • 19
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
1. 实验内容 每一个正规集都可以由一个状态数最少的DFA所识别,这个DFA是唯一的(不考虑同构的情况)。任意给定的一个DFA,根据以下算法设计一个C程序,将该DFA 化简为与之等价的最简DFA。 2. 实验设计分析 2.1 实验设计思路 根据实验指导书和书本上的相关知识,实现算法。 2.2 实验算法 (1)构造具有两个组的状态集合的初始划分I:接受状态组 F 和非接受状态组 Non-F。 (2)对I采用下面所述的过程来构造新的划分I-new. For I 中每个组G do Begin 当且仅当对任意输入符号a,状态s和读入a后转换到I的同一组中; /*最坏情况下,一个状态就可能成为一个组*/ 用所有新形成的小组集代替I-new中的G; end (3)如果I-new=I,令I-final=I,再执行第(4)步,否则令I=I=new,重复步骤(2)。 (4)在划分I-final的每个状态组中选一个状态作为该组的代表。这些代表构成了化简后的DFA M'状态。令s是一个代表状态,而且假设:在DFA M中,输入为a时有从s到t转换。令t所在组的代表是r,那么在M’中有一个从s到r的转换,标记为a。令包含s0的状态组的代表是M’的开始状态,并令M’的接受状态是那些属于F的状态所在组的代表。注意,I-final的每个组或者仅含F中的状态,或者不含F中的状态。 (5)如果M’含有死状态(即一个对所有输入符号都有刀自身的转换的非接受状态d),则从M’中去掉它;删除从开始状态不可到达的状态;取消从任何其他状态到死状态的转换。 。。。。。。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值