自动机是一种非常有力的工具,其完备的理论可以参考编译原理或者形式语言与自动机等相关教材。从某种定义角度而言,图灵机也是自动机的一种。
这里提到的自动机特指有限状态自动机,简称为FA,根据状态转移的性质又分为确定的自动机(DFA)和非确定的自动机(NFA)。FA的表达能力等价于正规表达式或者正规文法。
FA可以看做是一个有向带权图,图的顶点集合称为自动机的状态集合,图的权值集合为自动机的字母集合,图的边代表了自动机中状态变化的情况。此外,根据需要,自动机还需指定初始状态和终态。
FA最基本的作用就是形式化描述,而且有利于编程实现。以下提到自动机全部指的是DFA。
考虑仅由字母{a, b}组成的字符串,要求字符串中字母b必须成对出现,否则字符串非法。这个规则实现起来其实非常简单,不需要自动机也完全可以。但是我们考虑使用自动机来进行判断。顺便,该规则的正规表达式描述是:(a|bb)*。星号运算代表重复若干次,包括零次。
我们考虑做一个图来表示描述该规则的DFA。
令状态1为初始状态,显然在状态1上,我们还没有违反规则。因此,经过字母a以后我们还可以回到状态1。经过字母b就不能回到状态1了,此时需要一个新状态,令其为2。
状态2表示“待定的”状态,在这个状态时不能肯定字符串是非法的,但也不是合法的。在状态2上,如果经过字母b,就回到了合法的状态也就是状态1,;如果经过字母a,则该字符串肯定是非法的。建立一个新状态即状态3,用于表示非法状态。
状态3比较简单,已经到了非法状态,其后的任何字母都不会改变这个状态了。
因此,该DFA可以表示如下。
程序实现也非常简单,状态和字母都被编码成整数,使用一个矩阵表示状态转移,再写一个函数表示自动机的运行,对每一个字符串,从状态1开始运行,运行完毕进行状态判断即可。最后能停留在状态1的字符串才是符合规则的,其余都是非法的。
#include <cstdio>
int DFA[4][2] = {
{0},
{1,2},
{3,1},
{3,3}
};
int START_STATE = 1;
int run(char const word[]){
int state = START_STATE;
for(char const*p=word;*p;++p){
state = DFA[state][*p-'a'];
if ( 3 == state ) return state;
}
return state;
}
int main(){
char a[] = "abbaaa";
char b[] = "aabbba";
char c[] = "aaab";
printf("%d\n",run(a));
printf("%d\n",run(b));
printf("%d\n",run(c));
return 0;
}
DFA的使用,在一定程度上可以简化程序设计,将程序设计的关键步骤变为了自动机的设计。
考虑hdu2206,判断给定的字符串是否符合IPv4的规则。
为了方便起见,首先设计一个简单一点的DFA,可以判断出该字符串是否由4节组成,每节1到3位数字组成,节与节由小数点分开。
其中d表示0-9的十个数字。如果是小数点或者数字以外的字母全部去到错误状态,错误状态记作0,在图中未画出。
当格式符合该DFA标准后,再将数值超过255的剔除即可。使用DFA也可以直接判断诸如256、3xx等串不符合IPv4的格式标准,但是那样的话DFA就会更加复杂,不如直接使用数值判断方便。
#include <cstdio>
#include <algorithm>
using namespace std;
int const T[17][3] = {
// d . other
/*0*/ 0, 0, 0,//Error state
/*1*/ 2, 0, 0,
3, 5, 0,
4, 5, 0,
0, 5, 0,
/*5*/ 6, 0, 0,
7, 9, 0,
8, 9, 0,
0, 9, 0,
/*9*/ 10,0, 0,
11,13,0,
12,13,0,
0, 13,0,
/*13*/ 14,0, 0,
15,0, 0,//final state
16,0, 0,//final state
0, 0, 0 //final state
};
inline int tr(char ch){
if ( '0' <= ch && ch <= '9' ) return 0;
if ( '.' == ch ) return 1;
return 2;
}
int run(char const word[]){
int state = 1;
for(char const*p=word;*p;++p){
state = T[state][tr(*p)];
if ( 0 == state ) return state;
}
return state;
}
inline bool isFinal(int state){
return 14 == state || 15 == state || 16 == state;
}
char S[110];
int main(){
while ( gets(S) ){
if ( !isFinal(run(S)) ){
printf("NO\n");
continue;
}
int a,b,c,d;
sscanf(S,"%d.%d.%d.%d",&a,&b,&c,&d);
if ( a > 255 || b > 255 || c > 255 || d > 255 )
printf("NO\n");
else
printf("YES\n");
}
return 0;
}
再考虑POJ3332,判断是否为合法的数字。根据题目描述的规则,可以设计如下的DFA。其中前导空格事先排除,后续空格放入了DFA进行判断。
#include <cctype>
#include <cstdio>
inline int tr(char ch){
if ( isdigit(ch) ) return 0;
if ( '.' == ch ) return 1;
if ( 'E' == ch || 'e' == ch ) return 2;
if ( '+' == ch || '-' == ch ) return 3;
if ( ' ' == ch ) return 4;
return 5;
}
int DFA[10][6] = {
{0},
3,0,0,2,0,0,
3,0,0,0,0,0,
3,4,6,0,9,0,//final
5,0,0,0,0,0,
5,0,6,0,9,0,//final
8,0,0,7,0,0,
8,0,0,0,0,0,
8,0,0,0,9,0,//final
0,0,0,0,9,0 //final
};
int run(char const word[]){
int state = 1;
for(char const*p=word;*p;++p){
state = DFA[state][tr(*p)];
if ( 0 == state ) return state;
}
return state;
}
inline bool isFinal(int state){
return 3 == state || 5 == state || 8 == state || 9 == state;
}
char S[1005];
int main(){
int kase;
scanf("%d",&kase);
gets(S);
while(kase--){
gets(S);
char *p = S;
while( ' ' == *p ) ++p;
printf(isFinal(run(p))?"LEGAL\n":"ILLEGAL\n");
}
return 0;
}