给定一个上下文无关文法和句子,设计一个程序自动判定该句子是否是该文法产生?
1. 算法介绍
CYK(Cocke–Younger–Kasami)算法是一种用来对上下文无关文法(CFG,Context Free Grammar)进行语法分析(parsing)的算法。
CYK算法要求这个上下文无关文法满足乔姆斯基范式(CNF,Chomsky Normal Form)。对于任何不包含空串的CFG,它的产生式只有如下两种形式:
任何CFG都可以转换为乔姆斯基范式,其转换方法为:
CYK算法采用了自下而上的分析法,即从输入串出发,反复利用产生式做归约 ,自底向上逐步构造语法分析树,直至得到文法的开始符号。
同时,它采用了动态规划算法,用dp[i][j]是所有可以推导出word[i:j+1]的变元串的集合。根据这一定义,对角线上的元素dp[i][i]推导出的是单个符号,我们只需要根据符号找到能(直接)推导出它的变元,用以初始化dp数组:
for i, symbol in enumerate(word): # 遍历符号串中的符号 for leftpart,rightparts in grammar.items(): # 遍历产生式 if symbol in rightparts: dp[i][i].add(leftpart) # 初始化对角线上的元素为所有能直接推导产生当前终结符的变元
对于非对角线上的元素,可以递推产生:dp[i][j]就是对于每一个分割下标k(i\le k< j)能推导出word[i:k+1]和word[k+1:j+1]的变元串的集合,则有递推式:
根据递推式,要求解dp数组,需要按照先从下到上,再从左到右的顺序遍历:
for i in range(n-1,-1,-1): # dp顺序:从下到上 for j in range(i+1,n): # dp顺序:从左到右 for k in range(i,j): for leftpart,rightparts in grammar.items(): if set([leftVariable+rightVariable \ for leftVariable in dp[i][k] for rightVariable in dp[k+1][j]]) \ & rightparts: # 求两个集合的交,判断是否有相同元素 dp[i][j].add(leftpart) # 如果此变元可以直接推导出这两个变元,则加入这个变元
所以最终只需要判断开始符号是否在dp[0][n-1]中即可(因为它可以推导出word[0][n]):
return 'S' in dp[0][n-1] # 判断开始符号是否在dp数组右上角的元素中
2. 示例测试
解析平衡的括号表达式是一个经典的上下文无关文法的例子,其产生式如下:
首先需要将其化为CNF范式:
测试代码如下:
grammar = {'S': set(['SS', 'TR','LR']),'T': set(['LS']),'L': set(['(']), 'R': set([')'])} # 定义语法 print(cyk(grammar, '()()(()')) # 将语法和待检测串送入cyk算法并打印结果
输出False
print(cyk(grammar, '()(())()((()))'))
输出True
3. 完整代码
def cyk(grammar, word): n = len(word) dp = [[set() for j in range(n)] for i in range(n)] # 开辟一个n*n的dp数组,其中每个元素是一个集合 # 初始化 for i, symbol in enumerate(word): # 遍历符号串中的符号 for leftpart,rightparts in grammar.items(): # 遍历产生式 if symbol in rightparts: dp[i][i].add(leftpart) # 初始化对角线上的元素为所有能直接推导产生当前终结符的变元 # 遍历求解 for i in range(n-1,-1,-1): # dp顺序:从下到上 for j in range(i+1,n): # dp顺序:从左到右 for k in range(i,j): for leftpart,rightparts in grammar.items(): if set([leftVariable+rightVariable \ for leftVariable in dp[i][k] for rightVariable in dp[k+1][j]]) \ & rightparts: # 求两个集合的交,判断是否有相同元素 dp[i][j].add(leftpart) # 如果此变元可以直接推导出这两个变元,则加入这个变元 # 返回判断结果:True可以推导出,False不可推导出 return 'S' in dp[0][n-1] # 判断开始符号是否在dp数组右上角的元素中 # 测试 grammar = {'S': set(['SS', 'TR','LR']),'T': set(['LS']),'L': set(['(']), 'R': set([')'])} # 定义语法 print(cyk(grammar, '()(())()((()))')) # True