一. 枚举类型

枚举类型可以声明符号名称来表示 int 整形常量,枚举类型的目的是提高程序的可读性。

默认情况下,枚举列表中的常量是从 0 开始赋予的;也可以为枚举常量指定整数值

#include <stdio.h> 

enum States
{
    state_0,          //0
    state_1,          //1
    state_2,          //2
};

enum
{
    state_3 = 3,      //3
    state_4,          //4
    state_5,          //5
};

int main(void)
{
    printf("%d\n" , state_0);
    printf("%d\n" , state_1);
    printf("%d\n" , state_2);
    
    printf("%d\n" , state_3);
    printf("%d\n" , state_4);
    printf("%d\n" , state_5);
    return 0;
}

二. switch 语句

switch 语句的作用也是在多个选项中选择,虽然这可以用 if 语句实现,但在某些场合下,switch 语句更为方便

switch 语句语法

switch(expression)
{
    statement-list
}

贯穿于语句列表之间的是一个或多个 case 标签

case constant-expression:

常量表达式是指在编译期间进行求值的表达式,它不能是任何变量

不同寻常的是 case 标签并不把语句列表划分为几个部分,它们只是确定语句列表的进入点

#include <stdio.h>

enum Fruit
{
    apple,      
    banana,     
    orange,     
    pear        
};

int main(void)
{
    int fruit = apple;
    switch(fruit)
    {
        case apple:
            printf("I'm an apple.\n");
        case banana:
            printf("I'm a banana.\n");
        case orange:
            printf("I'm an orange.\n");     
        case pear:
            printf("I'm a banana.\n");      
        default:
            printf("No such fruit.\n");
    }
    return 0;
}

如果 switch 语句的执行遇到了 break 语句,执行流就会立即跳到语句列表的末尾。

否则,执行流从语句列表中 case 标签值与 expression 值匹配的语句,直到语句列表的结束,它们之间所有的语句均被执行。

#include <stdio.h>

enum Fruit
{
    apple,      
    banana,     
    orange,     
    pear        
};
 
int main(void)
{
    int fruit = apple;
    switch(fruit)
    {
        case apple:
            printf("I'm an apple.\n");
            break;
        case banana:
            printf("I'm a banana.\n");
            break;
        case orange:
            printf("I'm an orange.\n");
            break;
        case pear:
            printf("I'm a banana.\n");
            break;    
        default
            printf("No such fruit.\n");
            break;
    }
    return 0;
}

当表达式的值与所有的 case 标签的值都不匹配,就会执行 default 子句。

default 子句可以写在任何一个 case 标签出现的位置,这使得我们可以执行那些与 case 标签不匹配的表达式

#include <stdio.h>

enum Fruit
{
    apple,      
    banana,     
    orange,     
    pear        
};

int main(void)
{
    int fruit = 999;
    switch(fruit)
    {
        default:
            printf("No such fruit.\n");
        case apple:
            printf("I'm an apple.\n");       
        case banana:
            printf("I'm a banana.\n");        
        case orange:
            printf("I'm an orange.\n");
            break;       
        case pear:
            printf("I'm a banana.\n");
            break;      
    }
    return 0;
}

三. 有穷自动机简介

有穷自动机是许多重要类型的硬件和软件的有用模型

  1. 数字电路的设计和性能检查软件
  2. 编译器的 “词法分析器”
  3. 扫描大量文本来发现单词、短语出现的软件
  4. 只有有穷多个不同状态的系统(比如通信协议或安全交换信息的协议)的验证软件

自动机理论的中心概念

1.1 字母表

字母表是符号的有穷非空集合。约定俗成,用符号 $\sum$ 表示字母表。常见字母表包括:

  1. $\sum = \{0, 1\}$,二进制字母表
  2. $\sum = \{a, b, \cdots, z\}$,所有小写字母的集合

1.2 串

串是从某个字母表中选择的符号的有穷序列。例如,01101 是从二进制字母表 $\sum = \{0, 1\}$ 中选出的串,串 111 是从这个字母表中选择的另一个串

1.2.1 空串

空串是出现 0 次符号的串,这个串记作 $\epsilon$,是可从任意字母表中选择的串

1.2.2 串的长度

串的长度就是串中符号的位数。例如,01101 长度为 5。

串 w 的长度的标准记号是 $\lvert w \rvert$ 。例如, $\lvert 011 \rvert$ = 3,$\lvert \epsilon \rvert$ = 0

1.2.3 字母表的幂

如果 $\sum$ 是一个字母表,就可用指数记号来表示这个字母表某个长度的所有串的集合。定义 $\sum^{k}$ 是长度为 k 的串的集合,串的每个符号都属于 $\sum$

注意无论 $\sum$ 是什么字母表,$\sum^{0}$ = $\{ \epsilon \}$ 。就是说,$\epsilon$ 是长度为 0 的唯一的串

如果 $\sum = \{0, 1\}$,则 $\sum^{1}=\{0, 1\}$,$\sum^{2}=\{00, 11,01,10\}$,$\sum^{3}=\{000, 001,010,011,100,101,110,111\}$,依次类推。

字母表 $\sum$ 上所有串的集合约定记作 $\sum^{\ast}$ ,换句话说 $\sum^{\ast}=\sum^{0}\bigcup\sum^{1}\bigcup\sum^{2}\bigcup\cdots$

1.2.4 串的连接

设 x 和 y 都是串。于是,xy 表示 x 和 y 的连接。设 x = 01101 且 y = 110,则 xy = 01101110,yx = 11001101。对任意串 w,等式 $\epsilon w$=$w\epsilon$=$w$ 成立。

1.3 语言

$\sum$ 是某个具体的字母表,全部从 $\sum^{\ast}$ 中选出的串的一个集合称为语言。如果 $\sum$ 是字母表,且 $L \subseteq \sum^{\ast}$,则 L 是 $\sum$ 上的语言

注意,$\sum$ 上的语言不必包含带有 $\sum$ 所有符号的串,所以,一旦确定 L 是 $\sum$ 上的语言,也就知道了 L 是任何是 $\sum$ 的超集的字母表上的语言

语言可看作串的集合,一个例子就是英语,合法英语单词全体就是包含所有字母的字母表上的串的集合

1.4 问题

在自动机理论中,一个问题就是判定一个给定的串是否属于某个具体语言的提问。

将要看到,任何在口头上称为 “问题” 的东西,竟然都可以表示成语言的成员性

更准确地说,如果 $\sum$ 是字母表,L 是 $\sum$ 上的语言,则问题 L 就是:

🔥 给定 $\sum^{\ast}$ 中的一个串 w,判定 w 是否属于 L

语言和问题其实相同。倾向于使用哪种术语依赖于所采取的观点。当只关心串本身时,如在集合 $\{0^n1^n \mid n\geqslant1 \}$,,就倾向于串的集合是语言。当给串赋予了“语义”,如认为串编码了图、逻辑表达式甚至偶数等。在这些情况下,对串所表示的东西的关心超过了对串本身的关心,就倾向于串的集合是问题

四. 确定性有穷自动机的定义

“确定型”是指这样的事实,在每个输入上存在且仅存在一个状态,自动机可以从当前状态转移到这个状态

一个确定型有穷自动机(DFA)包括:

  1. 一个有穷的状态集合,通常记作 Q
  2. 一个有穷的输入符号集合,通常记作 $\sum$
  3. 一个转移函数,以一个状态和输入符号作为变量,返回一个状态。转移函数通常记作 $\delta$ 。在非形式化表示自动机的图中,用状态之间的箭弧和箭弧上的标记来表示 $\delta$ 。如果 q 是一个状态,a 是一个输入符号,则 $\delta(q, a)$ 是这样的状态 p,使得从 q 到 p 有带 a 标记的箭弧。
  4. 一个初始状态 $q_0$,是 Q 中状态之一
  5. 一个接受状态的集合 F。集合 F 是 Q 的子集

可以用 $A=(Q, \sum, \delta, q_0, F)$ 这个 “五元组” 记号来表示 DFA,其中 A 是 DFA 的名字

4.1 DFA 如何处理串

关于 DFA 需要理解的第一件事情是,DFA 如何决定是否“接受”输入符号序列。DFA 的“语言”是这个 DFA 接受的所有串的集合

假设 $a_1a_2\dots a_n$ 是输入符号序列。让这个 DFA 从初始状态 $q_0$ 开始运行。查询转移函数 $\delta$ ,比如说 $\delta(q_0, a_1)=q_1$ ,以找出 DFA 在处理了第一个输入符号 $a_1$ 之后进入的状态。处理下一个输入符号 $a_2$ ,求 $\delta(q_1, a_2)$ 的值,假设这个状态是 $q_2$ 。以这种方式继续下去,找出状态 $q_3,q_4,\dots, q_n$ ,使得对每个 $i$,$\delta(q_{i-1} , a_i)=q_i$ 。如果 $q_n \in F$ ,则接受输入 $a_1a_2\dots a_n$,否则就“拒绝”

4.2 转移图

一个 DFA 的转移图是如下定义的图: (a) 对 Q 中每个状态存在一个顶点

(b) 对 Q 中每个状态 q 和 $\sum$ 中每个输入符号 a,设 $\delta(q, a)=p$ 。于是转移图有从顶点 q 到顶点 p 的带 a 标记的箭弧。如果有几个输入符号都导致从 q 到 p 的转移,则转移图有一个由这些符号列表标记的箭弧

(c) 有一个进入初始状态 $q_0$ 的带 Start 标记的箭弧。这个箭弧没有任何出发顶点

(d) 对应于接受状态(属于 F 的那些状态)的顶点用双圆圈标记。不属于 F 的状态用单圆圈

4.3 转移表

转移表是习惯上对像 $\delta$ 这样有两个变量和一个返回值的函数的表格表示。这个表的各行对应着状态,各列对应着输入。在状态 q 对应的行和输入 a 对应的列这个位置上的项是状态 $\delta(q,a)$

  0     1  
-> $q_0$ $q_2$ $q_0$
* $q_1$ $q_1$ $q_1$
$q_2$ $q_2$ $q_1$

4.4 课堂练习

  1. 设计一个 DFA,以接受语言 $L=\{w \mid w形如x01y,x和y是只包含0和1的两个串\}$
#include <stdio.h>
#include <string.h>

enum state
{
    state_0,      
    state_1,            
    state_2,                    
};

bool isAcceptL(const char* str)
{
    int cur_state = state_0;
    for(int i = 0; i < strlen(str); ++i)
    {
        switch(cur_state)
        {
            case state_0:
                if(str[i] == '0')
                    cur_state = state_1;
                else if(str[i] == '1')
                    cur_state = state_0;
                break;

            case state_1:
                if(str[i] == '0')
                    cur_state = state_1;
                else if(str[i] == '1')
                    cur_state = state_2;
                break;

            case state_2:
                if(str[i] == '0')
                    cur_state = state_2;
                else if(str[i] == '1')
                    cur_state = state_2;
                break;
        }
    }

    if(cur_state == state_2)
        return true;
    else
        return false;
}

int main(void)
{
    const char* str = "11111111101";
    printf("IsAccept: %d\n", isAcceptL(str));
    return 0;
}

作业

  1. 设计一个 DFA,以接受语言 $L=\{ w \mid w 同时有偶数个 0 和偶数个 1 \}$

  2. 设计一个 DFA,以接受语言 $L=\{ w \mid w 形如 x00,x 是只包含0和1的串 \}$