使用CYK算法判断一个句子是否是上下文无关文法产生

给定一个上下文无关文法和句子,设计一个程序自动判定该句子是否是该文法产生?

1. 算法介绍

CYK(Cocke–Younger–Kasami)算法是一种用来对上下文无关文法(CFG,Context Free Grammar)进行语法分析(parsing)的算法。


CYK算法要求这个上下文无关文法满足乔姆斯基范式(CNF,Chomsky Normal Form)。对于任何不包含空串的CFG,它的产生式只有如下两种形式:


A \rightarrow a \\ A \rightarrow BC

任何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[i][j]=dp[i][k]+dp[k+1][j]

根据递推式,要求解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. 示例测试

解析平衡的括号表达式是一个经典的上下文无关文法的例子,其产生式如下:

S\rightarrow SS \\ S\rightarrow (S) \\ S \rightarrow ()

首先需要将其化为CNF范式:

S\rightarrow SS\ |\ TR\ |\ LR \\ T \rightarrow LS \\ L \rightarrow ( \\ L \rightarrow ) \\

测试代码如下:

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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值