编译原理的两个课程设计之一,关于两个正则表达式是否等价的问题。题目描述及提交地址:http://soj.me/show_problem.php?pid=1000&cid=866,大概内容如下:
Description
两个正则表达式等价,是指两个表达式描述完全相同的语言,即正则表达式expr1和expr2等价,当且仅当L(expr1)=L(expr2)。编写判断两个正则表达式是否等价的程序。
Input
有多组输入数据. 每组数据有两个正则表达式expr1和expr2,每个正则表达式占一行. expr1和expr2仅含有字符a,b,c,d,e,|,(,),*,+,?,其中a,d代表相应的字符(也即我们考虑的语言定义在字母表 ={a,d}上),而e代表空串є,其它符号的意义和“龙书”一致. expr1和expr2的长度不超过80,且均保证是合法表达式. 输入以一个'#'号结束.
Output
对于每组数据,如果expr1和expr2等价,则输出“YES”;否则,输出“NO”.
对于这个项目,复习相关知识用了一天半,敲代码用了一天,调试用了半天,前后大概三天可以完成。作业提交截止后,撰写此文供日后自己复习和回顾,供他人参考和点评。
需要注意的是,本人参考的书籍为《编译原理》(即“龙书”)第二版中文翻译版,代码有前后三个版本:alpha(有调试和跟踪信息),beta(能通过SOJ但没注释),gamma(有注释和过程输出),若想看初始代码请查看alpha版本,若想只求AC请查看beta版本,若想了解算法请查看gamma版本。本文使用的代码为gamma版本,而作为解释说明的版本,代码并不能够直接在Sicily上AC通过,也就是不会严格符合文章开始时所描述的大概内容。
书籍下载:http://download.csdn.net/detail/ederick/5043025
代码下载:http://download.csdn.net/detail/ederick/5043904
另外参考:http://mcs.sysu.edu.cn/user/chenzz/Article_1591
最后感谢王建同学和李晓潮同学的帮助,帮我解答了一些疑问以及提供了一些典型例子,避免了很多麻烦,多谢!
一些技巧和细节会在下文中说明,测试样例也会放出,请留意找寻。
下面开始程序流程的介绍:
要求是判断两个正则表达式是否等价,总的步骤可以划分为四部分:
1.正则表达式转换成NFA;
2.NFA转换成DFA;
3.DFA最小化;
4.判断两个最小化DFA是否等价。
各个部分大概的流程如下所示:
A. 正则表达式转换成NFA:
这一部分的代码实现基本上遵循龙书上第3.7.4章节中的算法3.23(P100),即McMaughton-Yamada-Thompson算法。子NFA的构造方法如书上所说,这里不再熬述。
需要说明的是,个人修改过其中的的一个部分,即把归约规则中的r = st,并没有按书上所说把N(s)的接受状态和N(t)的开始状态合并,而是在两者之间通过ε转换来连接。另外,对于书上没有描述的?和+也自行构造了NFA的模版。还有的是,本人也没有对正则表达式先进行后缀表示的转换,而是直接对其进行分析并构造NFA。在这里,r = st会当作r = s & t来处理。
基本思路是,创建两个栈分别用于存储操作符和子NFA,然后从头到尾开始遍历正则表达式。对于当前处理的字符,作以下判断和处理:
Ø 如果是字母,并且如果前一字符是abcde)*+?中的一个,进行如下操作:a.如果栈顶操作符是&,执行catNFA();否则跳过此步骤b.压入操作符&构造当前字母的NFA并压栈;
Ø 如果是操作符|,当栈非空并且栈顶不是&时,不断把栈顶的|或者&操作弹出来,并执行相应的orNFA()或者catNFA();最后压入|操作符;
Ø 如果是操作符?、*、+,则直接执行相应的queNFA()、starNFA()、plusNFA();
Ø 如果是(,并且如果前一字符是abcde)*+?中的一个,进行如下操作:a.如果栈顶操作符是&,执行catNFA();否则跳过此步骤b.压入操作符&最后将(压栈;
Ø 如果是),当栈顶不是(时,不断把栈顶的|或者&操作弹出来,并执行相应的orNFA()或者catNFA();最后把栈顶的(弹出。
说明一下,queNFA()、starNFA()、plusNFA()需要一个子NFA,而orNFA()、catNFA()需要两个子NFA。对于以上操作,是基于操作符的优先级顺序:(>*= + = ? >& >|>)的进行的。
这一部分的代码如下:
/*********************正则转NFA**********************************************/ unsigned N,M; //分别表示NFA和DFA的状态数 string s; //正则表达式 string sub( "abcde)*+?" ); //正则表达式中需要构造子NFA的字符 stack<char> sign; //存储操作符 stack< pair<int,int> > nfa; //存储子NFA vector<int> NFA[ 160 ][ 5 ]; //NFA的状态转移表 /*对于是字母(a、b、c、d、e)的NFA构造方法*/ void alpNFA( int i ) { pair<int,int> r; r.first = N++; r.second = N++; NFA[ r.first ][ i ].push_back( r.second ); nfa.push( r ); } /*对于是|的NFA构造方法*/ void orNFA() { sign.pop(); pair<int,int> r; r.first = N++; r.second = N++; pair<int,int> s = nfa.top(); nfa.pop(); pair<int,int> t = nfa.top(); nfa.pop(); NFA[ r.first ][ 4 ].push_back( s.first ); NFA[ r.first ][ 4 ].push_back( t.first ); NFA[ s.second ][ 4 ].push_back( r.second ); NFA[ t.second ][ 4 ].push_back( r.second ); nfa.push( r ); } /*对于是?的NFA构造方法*/ void queNFA() { pair<int,int> s = nfa.top(); nfa.pop(); NFA[ r.first ][ 4 ].push_back( s.first ); NFA[ r.first ][ 4 ].push_back( r.second ); NFA[ s.second ][ 4 ].push_back( r.second ); nfa.push( r ); } /*对于是&的NFA构造方法*/ void catNFA() { sign.pop(); pair<int,int> t = nfa.top(); nfa.pop(); pair<int,int> r( s.first,t.second ); NFA[ s.second ][ 4 ].push_back( t.first ); nfa.push( r ); } /*对于是*的NFA构造方法*/ void starNFA() { pair<int,int> s = nfa.top(); nfa.pop(); NFA[ r.first ][ 4 ].push_back( s.first ); NFA[ r.first ][ 4 ].push_back( r.second ); NFA[ s.second ][ 4 ].push_back( s.first ); NFA[ s.second ][ 4 ].push_back( r.second ); nfa.push( r ); } /*对于是+的NFA构造方法*/ void plusNFA() { pair<int,int> s = nfa.top(); nfa.pop(); NFA[ r.first ][ 4 ].push_back( s.first ); NFA[ s.second ][ 4 ].push_back( s.first ); NFA[ s.second ][ 4 ].push_back( r.second ); nfa.push( r ); } /*初始化NFA相关状态*/ void resetNFA() { N = 0; for ( int i = 0; i < 160; i++ ) { for ( int j = 0; j < 5; j++ ) NFA[ i ][ j ].clear(); } while ( !sign.empty() ) sign.pop(); while ( !nfa.empty() ) nfa.pop(); } /*正则表达式转NFA*/ void RE2NFA() { resetNFA(); /*遍历字符串,对相关字符进行对应操作*/ for ( unsigned i = 0; i < s.size(); i++ ) { if ( isalpha( s[ i ] ) ) { /*判断是否需要加上操作符&*/ if ( i != 0 && sub.find( s[ i - 1 ] ) != string::npos ) { if ( !sign.empty() && sign.top() == '&' ) catNFA(); sign.push( '&' ); } alpNFA( s[ i ] - 'a' ); } else if ( s[ i ] == '|' ) { while ( !sign.empty() && sign.top() != '(' ) { if ( sign.top() == '|' ) orNFA(); else if ( sign.top() == '&' ) catNFA(); } sign.push( '|' ); } else if ( s[ i ] == '?' ) queNFA(); else if ( s[ i ] == '*' ) starNFA(); else if ( s[ i ] == '+' ) plusNFA(); else if ( s[ i ] == '(' ) { /*判断是否需要加上操作符&*/ if ( i != 0 && sub.find( s[ i - 1 ] ) != string::npos ) { if ( !sign.empty() && sign.top() == '&' ) catNFA(); sign.push( '&' ); } sign.push( '(' ); } else if ( s[ i ] == ')' ) { while ( sign.top() != '(' ) { if ( sign.top() == '|' ) orNFA(); else if ( sign.top() == '&' ) catNFA(); } sign.pop(); } } /*清空操作符栈,获取最终NFA*/ while ( !sign.empty() ) { if ( sign.top() == '|' ) orNFA(); else if ( sign.top() == '&' ) catNFA(); } } /*输出NFA的各项数据*/ void showNFA() { cout << "**********第一步:NFA各项数据如下***************\n"; pair<int,int> r = nfa.top(); cout << "状态数:" << N << "\t"; cout << "开始状态:" << r.first << "\t" << "接受状态:" << r.second << endl; cout << "\ta\tb\tc\td\tε\n"; for ( unsigned i = 0; i < N; i++ ) { cout << i << ":\t"; for ( unsigned j = 0; j < 5; j++ ) { sort( NFA[ i ][ j ].begin(),NFA[ i ][ j ].end() ); cout << "{"; for ( unsigned k = 0; k < NFA[ i ][ j ].size(); k++ ) { cout << NFA[ i ][ j ][ k ]; if ( k + 1 != NFA[ i ][ j ].size() ) cout << ","; } cout << "}\t"; } cout << endl; } }
这里面有几个技巧和细节,罗列如下:
1.返回值采用pair<int,int>的形式是为了便于获取NFA的开始状态和接受状态;
2.NFA的数组开到160,是因为每个符号最多构造2个状态,80个字符就是160个状态;
3.?和+操作符的NFA构造可以等价于a?=(a|e),a+=aa*就可以很方便地得到了;
4.sub字符串的存在是为了便于判断是否需要添加&进操作符栈。
B. NFA转换成DFA
基本上如同龙书算法3.20(P97)的描述一样,通过子集构造算法,我们可以从一个NFA中构造出相应的DFA。这一部分按照书上的内容可以按部就班完成,如下:
其中的函数意义如下:
较为重要的计算e-closure(T)的方法如下:
D的开始状态是e-closure(S0),接受状态是所有至少包含了N的一个接受状态的状态集合。
这一部分的代码如下:
/*********************NFA转DFA************************************************/ map< vector<int>,int > trans; //NFA的状态和DFA的状态对应关系 vector<int> acc; //DFA中的接受状态 int DFA[ 1600 ][ 5 ]; //DFA的状态转换表 /*计算e-closure(T)*/ vector<int> eClosure( vector<int> T ) { stack<int> s; vector<int>::iterator it; int t; for ( it = T.begin(); it != T.end(); it++ ) s.push( *it ); while ( !s.empty() ) { t = s.top(); s.pop(); for ( it = NFA[ t ][ 4 ].begin(); it != NFA[ t ][ 4 ].end(); it++ ) { if ( find( T.begin(),T.end(),*it ) == T.end() ) { T.push_back( *it ); s.push( *it ); } } } sort( T.begin(),T.end() ); return T; } /*计算move(T,a)*/ vector<int> move( vector<int> T,int a ) { vector<int>::iterator it1,it2; vector<int> U; for ( it1 = T.begin(); it1 != T.end(); it1++ ) { for ( it2 = NFA[ *it1 ][ a ].begin(); it2 != NFA[ *it1 ][ a ].end(); it2++ ) { if ( find( U.begin(),U.end(),*it2 ) == U.end() ) U.push_back( *it2 ); } } return U; } /*初始化DFA数据*/ void resetDFA() { M = 0; trans.clear(); acc.clear(); } /*把NFA转换成DFA*/ void NFA2DFA() { resetDFA(); pair<int,int> r = nfa.top(); vector<int> T,U; T.push_back( r.first ); U = eClosure( T ); vector< vector<int> > Dstates; stack< vector<int> > s; Dstates.push_back( U ); s.push( U ); trans[ U ] = M++; if ( find( U.begin(),r.second ) != U.end() ) acc.push_back( M - 1 ); while ( !s.empty() ) { T = s.top(); s.pop(); for ( int a = 0; a < 4; a++ ) { U = eClosure( move( T,a ) ); if ( find( Dstates.begin(),Dstates.end(),U ) == Dstates.end() ) { Dstates.push_back( U ); s.push( U ); trans[ U ] = M++; if ( find( U.begin(),r.second ) != U.end() ) acc.push_back( M - 1 ); } DFA[ trans[ T ] ][ a ] = trans[ U ]; } } } /*输出DFA的各项数据*/ void showDFA() { cout << "**********第二步:DFA各项数据如下***************\n"; cout << "状态数:" << M << "\t"; cout << "开始状态:" << 0 << "\t" << "接受状态:"; for ( unsigned i = 0; i < acc.size(); i++ ) cout << acc[ i ] << " "; cout << endl; cout << "\ta\tb\tc\td\n"; for ( unsigned i = 0; i < M; i++ ) { cout << i << ":\t"; for ( int j = 0; j < 4; j++ ) cout << DFA[ i ][ j ] << "\t"; cout << endl; } }
技巧和细节说明如下:
1.DFA的状态上限设为1600没什么特别的原因,只是开大点以防内存溢出而已;
2.选用vector是因为状态数不确定,所以开数组不方便;而又不需要list的快速插入删除功能,综合权衡之下采取vector来存储数据;
3.可以留意到计算闭包后对vector进行了排序,这是为了后面计算得出来的vector能够有序,确保map中键值的比较正确,以及方便输出DFA状态转移表;
4.采用map是为了NFA和DFA中的状态得以对应,其中map对vector有很好的包容性,所以操作符[]使用起来相当方便;
C. DFA最小化
算法原则是基于龙书上算法3.39(P115),关键的分组部分如下:
代码如下:
/*********************DFA最小化***********************************************/ int groups[ 1600 ][ 5 ]; //输入符号状态转移到的分组 bool included[ 1600 ]; //状态的分组去向是否已确定 vector< vector<int> > PI,nPI; //原分组和新分组 int L; //最小化DFA的状态数 int MAP[ 1600 ]; //DFA和最小化DFA的状态对应关系 int MIN[ 1600 ][ 5 ]; //最小化DFA的状态转移表 int start; //最小化DFA的开始状态 vector<int> fin; //最小化DFA的接受状态 /*对状态划入分组*/ void grouping( int n ) { unsigned i,j,k; int t; vector<int> v; for ( i = 0; i < PI[ n ].size(); i++ ) { for ( j = 0; j < 4; j++ ) { t = DFA[ PI[ n ][ i ] ][ j ]; for ( k = 0; k < PI.size(); k++ ) { if ( find( PI[ k ].begin(),PI[ k ].end(),t ) != PI[ k ].end() ) { groups[ PI[ n ][ i ] ][ j ] = k; break; } } } } for ( i = 0; i < PI[ n ].size(); i++ ) { if ( !included[ PI[ n ][ i ] ] ) { v.clear(); v.push_back( PI[ n ][ i ] ); included[ PI[ n ][ i ] ] = true; for ( j = i + 1; j < PI[ n ].size(); j++ ) { if ( !included[ PI[ n ][ j ] ] ) { for ( k = 0; k < 4; k++ ) { if ( groups[ PI[ n ][ i ] ][ k ] != groups[ PI[ n ][ j ] ][ k ] ) break; } /*判断两个状态是否划入了同一个分组*/ if ( k == 4 ) { v.push_back( PI[ n ][ j ] ); included[ PI[ n ][ j ] ] = true; } } } nPI.push_back( v ); } } } /*初始化最小化DFA的各项数据*/ void resetMIN() { unsigned i,j; vector<int> v1,v2; for ( i = 0; i < M; i++ ) { if ( find( acc.begin(),acc.end(),i ) == acc.end() ) v1.push_back( i ); else v2.push_back( i ); } PI.clear(); nPI.clear(); nPI.push_back( v1 ); nPI.push_back( v2 ); } /*DFA转化成最小化DFA*/ void DFA2MIN() { resetMIN(); unsigned i,j; /*细分分组*/ memset( groups,sizeof( groups ) ); while ( PI.size() != nPI.size() ) { PI = nPI; nPI.clear(); memset( included,false,sizeof( included ) ); for ( i = 0; i < PI.size(); i++ ) grouping( i ); } /*确定状态对应关系*/ L = 0; for ( i = 0; i < PI.size(); i++ ) { for ( j = 0; j < PI[ i ].size(); j++ ) { MAP[ PI[ i ][ j ] ] = L; if ( PI[ i ][ j ] == 0 ) start = L; } L++; } /*获取接受状态*/ fin.clear(); for ( i = 0; i < acc.size(); i++ ) { if ( find( fin.begin(),fin.end(),MAP[ acc[ i ] ] ) == fin.end() ) fin.push_back( MAP[ acc[ i ] ] ); } sort( fin.begin(),fin.end() ); /*构造状态转移表*/ memset( included,sizeof( included ) ); for ( i = 0; i < M; i++ ) { if ( !included[ MAP[ i ] ] ) { included[ MAP[ i ] ] = true; for ( j = 0; j < 4; j++ ) MIN[ MAP[ i ] ][ j ] = MAP[ DFA[ i ][ j ] ]; } } } /*输出最小化DFA*/ void showMIN( int nMIN[ 2000 ][ 5 ],int nstart,vector<int> nfin,int nL ) { cout << "**********第三步:MIN各项数据如下***************\n"; cout << "状态数:" << nL << "\t"; cout << "开始状态:" << nstart << "\t" << "接受状态:"; for ( vector<int>::iterator it = nfin.begin(); it != nfin.end(); it++ ) cout << *it << " "; cout << endl; cout << "\ta\tb\tc\td\n"; for ( int i = 0; i < nL; i++ ) { cout << i << ":\t"; for ( int j = 0; j < 4; j++ ) cout << nMIN[ i ][ j ] << "\t"; cout << endl; } }
D. 判断两个最小化DFA是否等价
基本思路是通过对两个最小化DFA进行同步DFS遍历,而遍历的原则是:
对于两者访问的下一个节点:
1.都未被访问,则访问,进入深一层的遍历;
2.都已访问,改变字符输入寻求下一个未被访问的节点。
需要注意的是,当两者在遍历的过程中,碰到以下情况时:
1.其中一个到达了接受状态而另一个还处于非接受状态;
2.两者即将访问的下一个节点的深度不一样(都没被访问的话视作深度相同);
可以认为两个最小化DFA不是等价的。如果两者成功同步访问完所有的节点,即遍历过程中没有碰到以上的两种情况,就可以认为两个最小化DFA是等价的。
代码如下:
/*********************正则转最小DFA及比较*************************************/ int P,Q; //两个最小化DFA的状态数 int MIN1[ 1600 ][ 5 ],MIN2[ 1600 ][ 5 ]; //两个最小化DFA的状态转移表 int start1,start2; //两个最小化DFA的开始状态 vector<int> fin1,fin2; //两个最小化DFA的结束状态 int deep; //同步遍历时的深度 int vis1[ 1600 ],vis2[ 1600 ]; //两个最小化DFA在同步遍历时各个状态的深度 /*正则表达式转最小化DFA*/ void RE2MIN( int nMIN[ 1600 ][ 5 ],int &nstart,vector<int> &nfin,int &nL ) { RE2NFA(); showNFA(); NFA2DFA(); showDFA(); DFA2MIN(); memcpy( nMIN,MIN,sizeof( MIN ) ); nstart = start; nfin = fin; nL = L; } /*同步遍历*/ bool traversal( int n1,int n2 ) { int i,m1,m2; vis1[ n1 ] = vis2[ n2 ] = ++deep; for ( i = 0; i < 4; i++ ) { m1 = MIN1[ n1 ][ i ]; m2 = MIN2[ n2 ][ i ]; if ( find( fin1.begin(),fin1.end(),m1 ) == fin1.end() && find( fin2.begin(),fin2.end(),m2 ) != fin2.end() ) break; if ( find( fin1.begin(),m1 ) != fin1.end() && find( fin2.begin(),m2 ) == fin2.end() ) break; if ( vis1[ m1 ] != vis2[ m2 ] ) break; if ( vis1[ m1 ] == 0 && !traversal( m1,m2 ) ) break; } --deep; return i == 4; } /*判断两个正则表达式是否等价*/ bool equal() { if ( P != Q ) return false; memset( vis1,sizeof( vis1 ) ); memset( vis2,sizeof( vis2 ) ); deep = 0; return traversal( start1,start2 ); }
最后的主函数如下,大功告成:
/*********************主函数部分*********************************************/ int main() { while ( true ) { cout << "请输入第一个正则表达式:"; cin >> s; if ( s == "#" ) break; RE2MIN( MIN1,start1,fin1,P ); showMIN( MIN1,P ); cout << "\n请输入第二个正则表达式:"; cin >> s; RE2MIN( MIN2,start2,fin2,Q ); showMIN( MIN2,Q ); cout << "\n两个正则表达式的等价结果:"; cout << ( equal() ? "YES": "NO" ) << endl << endl << endl; } return 0; }
随便输入一个正则表达式,效果如下:
至此,所有工作完成。由于时间比较赶,代码当中应该存在不少错漏,效率上可能也不如人意,仍然有待改进。
附:测试样例和答案
a?
a|e YES
b*|a+
a+|b* YES
aa*|bb*
b+|a+ YES
e+++++*?*?*?++
eeee YES
(a|b)(a|b)
aa|ab|bb NO
(a|b)*
((e|a)b*)* YES
a+(aa)+
(aa)+a NO
a+(aa)+
(aa)+a+ YES
d*c+d?c*
d*c*d?c* NO
(a|b)|(c|d)
(c|d)|(a|b) YES
(a|b)*a(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)
(a|b)*b(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b) NO
(a|b)*a(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)
(a|b)*a(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b)(a|b) YES
b*a*b?a*
b*((a|ab)*|(a|ba)*) NO