Problem
问题
Loglan is a synthetic speakable language designed to test some of the fundamental problems of linguistics, such as the Sapir Whorf hypothesis. It is syntactically unambiguous, culturally neutral and metaphysically parsimonious. What follows is a gross over-simplification of an already very small grammar of some 200 rules.
Loglan是一种综合型可发音的语言,设计它是用来验证语言学上的一些原则性问题,比如萨皮尔—沃尔夫假说。它的句法精确明了,在文化上趋于中立,它的哲学是能省则省。该语言的语法集已经被过份的简化——只有200条语法规则。
Loglan sentences consist of a series of words and names, separated by spaces, and are terminated by a period (.). Loglan words all end with a vowel; names, which are derived extra-linguistically, end with a consonant. Loglan words are divided into two classes--little words which specify the structure of a sentence, and predicates which have the form CCVCV or CVCCV where C represents a consonant and V represents a vowel (see examples later).
Loglan的语句由一系列的单词和名称组成,中间由空格隔开,并由一个点号(.)表示结束。Loglan的单词均以元音结束;名称来自于该语言之外,由辅音结束。Loglan的单词分为两类——小单词和谓词。小单词指定了句子的结构;谓词的形式为“CCVCV”或“CVCCV”,其中C代表一个辅音,V代表一个元音。(见下面的例子)
The subset of Loglan that we are considering uses the following grammar:
我们考虑使用Loglan语言的一个子集,具有以下语法定义:
A → a | e | i | o | u
MOD → ga | ge | gi | go | gu
BA → ba | be | bi | bo | bu
DA → da | de | di | do | du
LA → la | le | li | lo | lu
NAM → {all names}
PREDA → {all predicates}
<sentence> → <statement> | <predclaim>
<predclaim> → <predname> BA <preds> | DA <preds>
<preds> → <predstring> | <preds> A <predstring>
<predname> → LA <predstring> | NAM
<predstring> → PREDA | <predstring> PREDA
<statement> → <predname> <verbpred> <predname> | <predname> <verbpred>
<verbpred> → MOD <predstring>
(译注:上面文法中NAM为所有名称的符号,PREDA为所有谓词的符号)
Write a program that will read a succession of strings and determine whether or not they are correctly formed Loglan sentences.
写一个程序,读入一组字符串并确定它们是不是正确的Loglan语句。
Input and Output
输入与输出
Each Loglan sentence will start on a new line and will be terminated by a period (.). The sentence may occupy more than one line and words may be separated by more than one whitespace character. The input will be terminated by a line containing a single `#'. You can assume that all words will be correctly formed.
每个Loglan语句均从新的一行开始,并以句点(.)结束。一条语句可能占用多行,且单词之间可能会多于一个空格。所有的输入由一个独占一行的#号表示结束。你可以认为所有单词的格式都是正确的。
Output will consist of one line for each sentence containing either 'Good' or 'Bad!'.
对于每一个输入的语句,应输出"Good"或"Bad!"。
Sample input
输入示例
la mutce bunbo mrenu bi ditca.
la fumna bi le mrenu.
djan ga vedma le negro ketpi.
#
Sample output
输出示例
Good
Bad!
Good
Analysis
分析
对于没学过编译原理的同学来说,这道题的难度比较大,下面将用尽量通俗的语言进行解析,相关的概念会进行简要的介绍。
题目中的语法定义由“产生式”给出,这里面用到了正则表达式的一些基本符号。一个产生式定义一个构造(推导)规则,前面是可推导出的符号,箭头后面指向的是符号序列,竖线“|”表示“或着”。举例而言:
A → a | e | i | o | u
上式的义意是a、e、i、o、u中任意一个符号都可以用符号A来代替(推导出A)。再看:
<statement> → <predname> <verbpred> <predname> | <predname> <verbpred>
上式的义意是:<predname> <verbpred> <predname>三个符号从左至右依次排列,可以用符号<statement>来代替,注意到“|”符号前后都有<predname> <verbpred>,即不管<verbpred>后面有没有<predname>作后缀,只要有<predname>前缀就可以推导出<statement>。但如果有<predname>后缀,是否一并推导就要看制定的推导策略了。题目要求判断任意给定的句子的语法是否正确,也就是要用该文法来推导句子,如果能推出<sentence>符号就算是正确的。
通过对产生式进行语法分析可知,该文法不存在二义性,但它并不是一个正则文法,需要对其进行转换。文法中PREDA是终结符号(除了谓词或名称外,没有其它符号可以推导出它们),它可以推导出<predstring>,而<predstring>又可以作为PREDA的前缀推导出<predstring>,那么<predstring>的正则表达式应该为:
PREDA+
上式中“+”号的意思是一个或多个PREDA连起来形成的串。我们还可以看到,<predstring>除了在它自身的产生式之外,其它地方都是用于后缀,因此多个连续的<predstring>可以直接合并,不会造成二义性问题。那么产生应改写为:
PREDA → PREDA PREDA
这样所有连续的多个PREDA都生成为单个PREDA,再转为<predstring>即可:
<predstring> → PREDA
NAM、DA和BA也是终结符,但它们的产生式没有自循环,无需转换。具体要说明什么是自循环,则要扯出有限自动机(DNF)、闭包、状态图等一大堆概念了,这些不是本文的重点,恕不赘述!还要需要处理的是符号A,<predstring>可以直接推导出<preds>,而<preds>又可以作为A的前缀推导出<preds>自己,那么<preds>的产生式可以改写为:
<predstring> → <predstring> A <predstring>
<preds> → <predstring>
画个状态图,一看便知。最后一个要处理的是:
<statement> → <predname> <verbpred> <predname> | <predname> <verbpred>
显然<predname>和<verbpred>可以作为一个整体,为了简化产生式,我们另设一个符号<predverb>:
<predverb> → <predname> <verbpred>
这样statement的产生式就可改写为正则文法:
<statement> → <predverb> <predname> | <predverb>
下面是处理好的正则文法:
A → a | e | i | o | u
MOD → ga | ge | gi | go | gu
BA → ba | be | bi | bo | bu
DA → da | de | di | do | du
LA → la | le | li | lo | lu
NAM → {all names}
PREDA → {all predicates} | PREDA PREDA
<sentence> → <statement> | <predclaim>
<predclaim> → <predname> BA <preds> | DA <preds>
<preds> → <predstring>
<predname> → LA <predstring> | NAM
<predstring> → PREDA | <predstring> A <predstring>
<predverb> → <predname> <verbpred>
<statement> → <predverb> <predname> | <predverb>
<verbpred> → MOD <predstring>
根据这个文法就可以快速的判断语句的正确性了。先将输入的所有字符串转换为单词(包括谓词和小单词)或名称,可根据最后一个字母是否元音来判断是单词还是名称,如果是名称就直接转为NAM,如果是单词则需按上面的文法查表(有不用查表的小技巧,详见代码)。整个句子都转换完成后,就要按照顺序对符号进行推导,过程如下:
- PREDA → PREDA PREDA
- <predstring> → PREDA
- <predname> → NAM
- <predname> → LA <predstring>
- <verbpred> → MOD <predstring>
- <predstring> → <predstring> A <predstring>
- <preds> → <predstring>
- <predclaim> → DA <preds>
- <predclaim> → <predname> BA <preds>
- <predverb> → <predname> <verbpred>
- <statement> → <predverb> <predname>
- <statement> → <predverb>
- <predclaim> → <sentence>
- <statement> → <sentence>
如果最后能推得<sentence>,说明句法是正确的,否则就是错误的。推导的具体实现方法其实就是对动态数组进行插入和删除的操作:如果一个符号可以直接推导出另一个符号,则直接改写为推导出的符号;如果一个符号和前缀一起能推导出另一个符号,就将前缀删除,当前符号改写为推导出的符号;后缀的情况还有前后缀一起推导的情况也是一样。具体算法详见代码的注释,非常详细。
程序中状态转换表的顺序与上述推导顺序相同,但这个顺序中很多地方是可以改变的。比如第3步就可以提前到第1步上面来完成,第13步也可以提前到第10步上面来完成。安排顺序的条件是在执行这一步前,箭头后面的符号序列已经完全推导,也就是说不存在任何产生式可以再产生箭头后面的任何一个符号。这样可以保证当前符号的推导一次性完成。如果没有保证这样的条件,则需在后面反工,还可能出错。比如:如果在第7步之前执行第8步,但此时句中尚未推导出符号<preds>,因此符号DA根本不可能成功的推导出<predclaim>。再比如:在执行6步前如果先执行了第7步,所有的<predstring>都推导成了<preds>,此时第6步就永远无法完成了,因为A必须靠<predstring>作前后缀才能进行推导。
在程序的实现中,一个符号作为前缀或作为后缀进行推导都是可以的,比如可以将<predname>作为<predverb>的后缀推出<statement>,也可将<predverb>作为<predname>的前缀作同样的推导。
符号枚举类型中的符号名为符号的缩写,对应如下:
UN: 未知/出错
PS: predstring
P: preds
PN: predname
SE: sentence
VP: verbpred
PV: predverb
PC: predclaim
ST: statement
其它符号名与题目中给出的相同。
Solution
解答
#include <iostream> #include <string> #include <vector> using namespace std; //各种符号的枚举,后面的注释为题目中对应的符号 enum SYMBOL{A, MOD, LA, BA, DA, PREDA, NAM, SE, PC, P, PN, PS, ST, VP, PV, UN}; bool aVowel[] = {1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0}; //元音表 static SYMBOL aConvTbl[14][4] = { //状态转换表,每四个状态为一组,顺序排列 {PREDA, UN, PREDA, PREDA}, {PREDA, UN, UN, PS}, {NAM, UN, UN, PN}, {LA, UN, PS, PN}, {MOD, UN, PS, VP}, {A, PS, PS, PS}, {PS, UN, UN, P}, {DA, UN, P, PC}, {BA, PN, P, PC}, {VP, PN, UN, PV}, {PV, UN, PN, ST}, {PV, UN, UN, ST}, {PC, UN, UN, SE}, {ST, UN, UN, SE}, //每组四个符号 }; //1:初始符号,2:前缀,3:后缀,4:推导出的符号 //将输入的字符串转换为符号的函数 SYMBOL Token2Status(const string &str) { int nNum = str.length(), cLast = str[nNum - 1]; if (!islower(cLast) || !aVowel[cLast - 'a']) { return NAM; //末尾不是元音的为NAM } switch (nNum) { case 1: return A; //只有一位元音的只能是A case 5: //用位运算快速判断谓词是否符合规则CCVCV或CVCCV nNum = aVowel[str[4] - 'a']; nNum |= ((aVowel[str[0] - 'a'] << 4) | (aVowel[str[1] - 'a'] << 3)); nNum |= ((aVowel[str[2] - 'a'] << 2) | (aVowel[str[3] - 'a'] << 1)); return (nNum == 5 || nNum == 9) ? PREDA : UN; case 2: //两位的单词 switch (str[0]) { //根据第一位判断是哪一组 case 'g': return MOD; case 'b': return BA; case 'd': return DA; case 'l': return LA; } } return UN; //未能识别的错误符号 } //主函数 int main(void) { vector<SYMBOL> Set; for (string str; cin >> str && str != "#";) { //循环读入每个单词 int nDot = str.find('.'); //如果单词中发现句点,则认为句子结束 if (nDot == str.npos) { //没有发现句点 Set.push_back(Token2Status(str)); //将单词转为符号后存入语句 continue; } //以下为发现句点,即遇到句子结束 str.erase(str.length() - 1); //删除句点 if (!str.empty()) { //单词不为空则加入语句 Set.push_back(Token2Status(str)); } //以下进行词法分析并输出结果 for (int i = 0; i < 14; ++i) { //依次处理每一种状态 SYMBOL *pTbl = aConvTbl[i]; //为加快运算,节省代码,设临时变量 for (vector<SYMBOL>::iterator j = Set.begin(); j != Set.end();) { vector<SYMBOL>::iterator iBeg = Set.begin(), iEnd = Set.end(); if (*j != pTbl[0]) { ++j; //不是指定符号,遍例下一个 continue; } //如果指定了前面或后面相邻的符号则验证其是否存在 if (pTbl[1] != UN && (j == iBeg || *(j - 1) != pTbl[1])) { ++j; //存在的符号与指定的不符,结果错误 continue; } if (pTbl[2] != UN && (j == iEnd - 1 || *(j + 1) != pTbl[2])) { ++j; //存在的符号与指定的不符,结果错误 continue; } //删除前后的符号(如果指定) j = pTbl[1] != UN ? Set.erase(j - 1) : j; j = pTbl[2] != UN ? Set.erase(j + 1) - 1 : j; *j = pTbl[3]; //当前符号变更为指定的目标符号 } } cout << (Set.size() == 1 && Set[0] == SE ? "Good" : "Bad!") << endl; Set.clear(); //清空语句,准备处理下一条语句 } return 0; }
![]() |
作者:王雨濛;新浪微博:@吉祥村码农;来源:《程序控》博客 -- http://www.cnblogs.com/devymex/ 此文章版权归作者所有(有特别声明的除外),转载必须注明作者及来源。您不能用于商业目的也不能修改原文内容。 |