形式语言与自动机|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类 与 状态结点类、窗口类各自执行自己的功能,数据和视图分离。


posted @ 2019-12-08 19:40  fishers  阅读(...)  评论(...编辑  收藏