形式語言與自動機|DFA識別句子


實驗二 DFA識別句子

一、實驗目的

加深對DFA工作原理的理解。

二、實驗內容

  • 1.設計固定DFA。也就是說用if-then-else(一般用來實現字母表中只有兩個字母的情況)、switch(大於兩個字母的情況)、for(用於控制輸入字符串,長度為n的字符串,for循環n次)等語句表示DFA。一個函數定義一個DFA;
  • 2.設計文件形式存儲DFA。設計文件格式,DFA動態生成,使用字符串來驗證DFA的有效性和正確性;(使用面向對象的方法。對於k個狀態的DFA,生成相應的k個狀態對象;狀態轉換應通過對象間的消息傳遞來實現)
  • 3.圖形化表示。用java或者VC中圖形功能實現圖形化的dfa。(選作)

前置知識1:DFA

什么是FA,也叫有窮狀態自動機;書上是這么說的👇,是一個五元組(狀態集合,字母表,狀態轉移表,開始狀態,終止狀態集合)





什么是DFA,也是一個五元組,在FA的基礎上加了一個約束條件:每一個狀態結點只能發出一條具有相同符號的邊;也就是同一狀態不能發出(輸入字符相同的)兩條邊上。可以發出輸入字符不同的多條邊


下圖就是一個DFA栗子👇




下圖就是NFA的栗子👇(允許從一個狀態發出多條具有相同符號的邊,甚至允許發出標有ε(表示空)符號的邊)

完成這個實驗,只需要知道DFA就可以了。

前置知識2:有向圖

什么是有向圖:由頂點和有方向的邊構成的圖



如何在程序中存儲有向圖?
可以使用數據結構中學的“鄰接矩陣”
“鄰接矩陣”就是一個“二維表”

DFA實質就是一個有向圖,各個頂點和其它頂點之間,使用有向邊相連接;而DFA的狀態轉移表,就是它的鄰接矩陣。


核心遞歸框架

目的:DFA識別句子

先明晰目的:判斷讀入的句子是否能被我們設定的DFA識別
比如一個DFA功能是“識別偶數個0的句子”;那么讀入句子“00”,偶數個0就能被識別,讀入“000”,奇數個0就不能被識別。

什么情況下識別了句子:能到達終結點

什么情況下能被識別?:從初始狀態讀入一個句子,最終能到達終結狀態結點,就是被識別了。
什么情況下不能被識別?:從初始狀態讀入一個句子,到最后到達非終結狀態結點,就是不能被識別。

DFA的有向圖上如何判斷識別了句子?

“是否能被識別”,在DFA的有向圖上如何表示?就是,從初始結點q0,一次讀句子的一個字符,最終到達了終結狀態。

還是那個栗子:識別偶數個0的DFA,假如我們輸入的句子是 “00”👇


還是那個栗子:識別偶數個0的DFA,假如我們輸入的句子是 “000”👇


核心遞歸框架偽代碼

下面兩個方法都可以

第一種遞歸框架:遞歸參數:狀態結點q1,讀入的剩余句子字符

//遞歸參數:狀態結點q1,讀入的剩余句子字符 
bool dfs(node 結點q1,string 句子sentence){
	
	//1.遞歸出口:當句子為空 或者 句子長度為0的時候 
	if(sentence == NULL || sentence.length() == 0){
		if(結點q1 是 終結點){
			return true; //最后到達終結點;表示句子可以被這個dfa識別  
		}else{
			return false; //否則 是非終結點;表示句子不能被這個dfa識別 
		}
	}
	
	//讀入sentence的第一個字符后,到達新的狀態結點q2 
	node q2 = 狀態轉移(q1,sentence[0]) //q1讀入一個字符到達q2
	return dfs(q2,"去除sentence第一個字符的剩下句子") //下一次遞歸參數: 結點q2,去除sentence第一個字符的剩下句子 
} 

把句子sentence設置為全局變量

第二種遞歸框架:遞歸參數:結點q1,當前讀入的字符在"讀入的句子"中的下標

//或者把句子sentence設置為全局變量
//下面一種遞歸思路,更換了遞歸參數,可能更容易理解 
//遞歸參數:結點q1,當前讀入的字符在"讀入的句子"中的下標 
bool dfs(node q1,int index){
 
	//1.遞歸出口:當讀入到句子的最后一個字符了,這時到達遞歸出口,判斷轉移后的結點是否是終結點 
	if(index == sentence.length() -1){
		node q2 = 狀態轉移(q1,sentence[index]);  //q1讀入一個字符到達q2
		if(結點q2 是 終結點){
			return true; //最后到達終結點;表示句子可以被這個dfa識別  
		}else{
			return false; //否則 是非終結點;表示句子不能被這個dfa識別 
		}
	}
	
	//狀態轉移  從q1讀入字符sentence[indx] 到達q2 
	node q2 = 狀態轉移(q1,sentence[index]);  //q1讀入一個字符到達q2
	return dfs(q2,index+1); //繼續遞歸 
} 

開始寫代碼1.設計固定DFA

采用面向對象的方式編程,沒有對象就new一個。

1-1:先寫一個狀態結點類

狀態結點類的整體構造是這樣的👇

1-2:DFA類

DFA的整體構造是這樣的👇

5元組對應5個屬性,其它還應該有狀態表結點個數、字母表字符個數、最大存儲的結點個數等屬性。👇


通過書上的一個DFA例子,理解一下狀態集合、終結點集合、字母表集合是這樣存儲的👇

1-3:幾個必要的函數


1-4:選一個栗子

選用例3-1 有窮自動機M: ({q0,q1,q2},{0},轉換函數,q0,{q2}),作為樣例
這個自動機功能是:識別偶數個0,比如00是合法的句子;而000就是非法的句子。

狀態轉換表如下


下圖是例題3-1的DFA圖👇


init()函數初始化例3-1的自動機👇,把例3-1的五元組分別存儲到實例對象的5個屬性中




graphInit()函數初始化狀態轉換表



主要理解狀態轉換表(有向圖的鄰接矩陣)👇

狀態集合、終結點集合、字母表集合是這樣存儲的👇


**然后試着理解狀態轉換表的存儲**👇

1-5:核心,遞歸程序識別句子


**run()啟動函數**👇



dfs()核心遞歸程序👇

1-6:例3-1栗子運行的效果


例3-1的DFA,功能:只識別偶數個0的句子。
主函數:




運行效果:


1-7:再舉一個栗子,會不會更清楚一點


例3-3的DFA,**功能:識別末尾是000的句子。**比如:110000是能夠識別正確的;而10100,因為末尾只有兩個0所以無法識別

例3-3DFA如下圖👇


把例3-3的五元組,在計算機中存儲,**“形式化”**,今天課上學到的名詞。。還是動詞??




對應在代碼中就是這樣的👇,修改init()函數,初始化DFA實例對象的五元組





Main函數中創建DFA類的實例化對象,init2()函數讀入例3-3的dfa五元組;再調用run()啟動程序,輸入要識別的句子即可。


###運行效果如下:

例3-3的DFA,功能:識別末尾是000的句子。

所以👇

輸入句子'111000',識別成功

輸入句子'10100',識別出錯

輸入句子'101011000',識別成功

輸入句子'1111',識別出錯


綜上,實現了,一個函數對應一個固定DFA。

開始寫代碼2.文件形式存儲DFA


步驟主要是這樣的: * 1.從文件中讀取每一行 * 2.把每一行,字符串處理,解析成五元組;比如,第一行是狀態結點,就解析為結點,存儲到狀態結點集合中。 * 3.Main函數中運行run(),輸入句子

我的文件里的五元組格式是這樣的👇

字符串處理,比較繁瑣,需要解析各個字符,讀入到 DFA類的五元組中;當然如果文件存儲的格式簡單一點,就不用像我下面寫的這樣復雜了
每次讀一行
這里用,(){}等等這些字符來分隔,狀態節點、字符、轉換函數

從文件中讀入DFA的,僅供參考的 “從文件中讀入DFA,存儲dfa的五元組” 代碼如下👇

	
	public void loadFromFile(String path) {
		File file = new File(this.path);
		StringBuffer result = new StringBuffer();
		List<String> resultList = new ArrayList<String>();
		this.clear();
		try {
			BufferedReader bReader = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));
			String tempString = null;
			while((tempString = bReader.readLine()) != null) {
				resultList.add(tempString);
			}
			bReader.close();
			String stateNodesString = resultList.get(0);
			String alphabetString = resultList.get(1);
			String initNodeString = resultList.get(2);
			String finalString = resultList.get(3);
			if(dealInputString(stateNodesString,0) == false) {
				System.out.println("DFA有誤!");
			}
			if(dealInputString(alphabetString,1) == false) {
				System.out.println("DFA有誤!");
			}
			if(dealInputString(initNodeString, 2) == false) {
				System.out.println("DFA有誤!");
			}
			if(dealInputString(finalString, 3) == false) {
				System.out.println("DFA有誤!");
			}
			if(dealFunction(resultList) == false) {
				System.out.println("DFA有誤!");
			}
		}catch (Exception e) {
			System.out.println(e);
			System.out.println("加載文件出錯");
		}
	}
	
	private Boolean dealFunction(List<String> resultList) {
		//從resultList的第四行開始是 狀態轉移函數
		this.graphInit();
		if(resultList.size() <= 4) return false;
		for(int i=4;i<resultList.size();i++) {
			String function = resultList.get(i);
			if(dealLine(function) == false) return false;
		}
		return true;
	}

	private Boolean dealLine(String function) {
		StringBuffer currentBuffer = new StringBuffer();
		int charIndex = -1;
		int nodeIndex1 = -1;
		int nodeIndex2 = -1;
		for(int i=0;i<function.length();i++) {
			if(function.charAt(i) == '(' ||function.charAt(i) == ')' || function.charAt(i) == '>' || function.charAt(i) == '-') {
				currentBuffer.delete(0, currentBuffer.length());
			}else if(function.charAt(i) == ',' ) {
				//這里是第一個結點
				nodeIndex1 = findNodeIndexByName(currentBuffer.toString());
				if(nodeIndex1 == -1) return false;
				currentBuffer.delete(0, currentBuffer.length());
			}else if(i == function.length()-1) {
				//這里是第二個結點
				currentBuffer.append(function.charAt(i));
				nodeIndex2 = findNodeIndexByName(currentBuffer.toString());
				if(nodeIndex2 == -1) return false;
				currentBuffer.delete(0, currentBuffer.length());
			}else if(i>0 && function.charAt(i-1) == ','){
				//這里是字符
				Character c = function.charAt(i);
				charIndex = getCharIndex(c);
				if(charIndex == -1) return false;
			}else {
				currentBuffer.append(function.charAt(i));
			}
		}
		if(nodeIndex1 == -1 || nodeIndex2 == -1 || charIndex == -1) return false;
		this.graph[nodeIndex1][charIndex] = nodeIndex2;
		System.out.println(stateNodes.get(nodeIndex1).getName()+"," + alphabet.get(charIndex) +" -> "+stateNodes.get(nodeIndex2).getName());
		return true;
	}

	private void clear() {
		// TODO Auto-generated method stub
		this.stateNodes.clear();
		this.alphabet.clear();
		this.finalNodes.clear();
		this.graphInit();
	}

	public boolean dealInputString(String s,int type) {
		if(s == null) return false;
		StringBuffer currentStateBuffer = new StringBuffer();
		
		//type == 0時 處理的是 狀態結點集合: 把輸入的一行字符串轉換為狀態結點,並存入狀態結點集合stateNodes中
		if(type == 0) {
			for(int i=0;i<s.length();i++) {
				if(s.charAt(i) == '{') {
					currentStateBuffer.delete(0, currentStateBuffer.length());
				}else if(s.charAt(i) == ',' || s.charAt(i) == '}') {
					this.stateNodes.add(new Node(currentStateBuffer.toString()));
					currentStateBuffer.delete(0, currentStateBuffer.length());
				}else {
					currentStateBuffer.append(s.charAt(i));
				}
			}
			this.nodesNum = stateNodes.size();
			for(Node node: this.stateNodes) {
				System.out.println(node.getName());
			}
		}else if(type == 1) {
			for(int i=0;i<s.length();i++) {
				if(s.charAt(i) == '{') {
					continue;
				}else if(s.charAt(i) == ',' || s.charAt(i) == '}') {
					continue;
				}else {
					this.alphabet.add(s.charAt(i));
				}
			}
			this.alphabetNum = alphabet.size();
			for(Character c: this.alphabet) {
				System.out.println(c);
			}
		}else if(type == 2) {
			Node initNodeTemp = findNodeByName(s);
			if(initNodeTemp == null) return false;
			this.initNode = findNodeByName(s);
			System.out.println(findNodeByName(s).getName());
		}else if(type == 3) {
			for(int i=0;i<s.length();i++) {
				if(s.charAt(i) == '{') {
					currentStateBuffer.delete(0, currentStateBuffer.length());
				}else if(s.charAt(i) == ',' || s.charAt(i) == '}') {
					Node finalNode = findNodeByName(currentStateBuffer.toString());
					if(finalNode == null) return false;
					this.finalNodes.add(finalNode);
					currentStateBuffer.delete(0, currentStateBuffer.length());
				}else {
					currentStateBuffer.append(s.charAt(i));
				}
			}
			for(Node node: this.finalNodes) {
				System.out.println(node.getName());
			}
		}
		return true;
	}

	public Node findNodeByName(String name) {
		for(Node node : stateNodes) {
			//node.getName() == name false:原因 == 用來判斷引用
			if(node.getName().equals(name)) 
				return node;
		}
		return null;
	}
	
	public int findNodeIndexByName(String name) {
		int i = 0;
		for(Node node : stateNodes) {
			//node.getName() == name false:原因 == 用來判斷引用
			if(node.getName().equals(name)) 
				return i;
			i++;
		}
		return -1;
	}

運行效果和第一問相同:



3.圖形化表示DFA


用Graphics類,重寫paint方法,paint方法就是在窗口界面上畫圖。
詳細可以參考這個鏈接,很簡單但具體的栗子


這里用面向對象編程,就是寫幾個類,DFA類 與 狀態結點類、窗口類各自執行自己的功能,數據和視圖分離。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM