题目理解

本题要求判断给定的字符串是否属于某个上下文无关文法(Context-Free Grammar\texttt{Context-Free Grammar}Context-Free Grammar, CFG\texttt{CFG}CFG)生成的语言,且该文法已转换为乔姆斯基范式(Chomsky Normal Form\texttt{Chomsky Normal Form}Chomsky Normal Form, CNF\texttt{CNF}CNF
CNF\texttt{CNF}CNF 文法中的所有产生式规则只有两种形式:

  1. A→BCA \to BCABC (其中 A,B,CA, B, CA,B,C 均为非终结符)
  2. A→aA \to aAa (其中 AAA 为非终结符,aaa 为终结符)

给定一个 CNF\texttt{CNF}CNF 文法(包括根符号、非终结符集、终结符集、产生式规则)和若干个候选字符串,我们需要对每个候选字符串输出它是否属于该文法生成的语言 L(G)L(G)L(G)


算法选择:CYK\texttt{CYK}CYK 算法

由于 CNF 形式具有很好的结构性质,我们可以使用 CYK\texttt{CYK}CYK 算法(Cocke–Younger–Kasami\texttt{Cocke–Younger–Kasami}CockeYoungerKasami 来解决这个问题。
CYK\texttt{CYK}CYK 算法是一种基于动态规划的算法,专门用于判断给定字符串是否属于某个 CNF\texttt{CNF}CNF 文法生成的语言。

CYK\texttt{CYK}CYK 算法核心思想

设候选字符串 sss 的长度为 nnn 。我们定义一个二维表 dp[i][j]dp[i][j]dp[i][j] ,它表示子串 s[i…j]s[i \dots j]s[ij] 可以由哪些非终结符推导出来(即存在至少一种推导方式从该非终结符出发得到这个子串)。

1. 初始化(长度为 1 的子串)

对于每个位置 iii0≤i<n0 \le i < n0i<n),考虑单个字符 s[i]s[i]s[i]
我们检查所有形如 A→aA \to aAa 的产生式,如果 a=s[i]a = s[i]a=s[i] ,那么 AAA 就可以推导出子串 s[i…i]s[i \dots i]s[ii]
因此,将 AAA 加入 dp[i][i]dp[i][i]dp[i][i] 中。

2. 动态规划递推(长度 > 1 的子串)

对于长度 len=2…nlen = 2 \dots nlen=2n 的子串 s[i…j]s[i \dots j]s[ij] (其中 j=i+len−1j = i + len - 1j=i+len1),我们尝试所有可能的分割点 kkki≤k<ji \le k < jik<j),将子串分为两部分:

  • 左部分:s[i…k]s[i \dots k]s[ik]
  • 右部分:s[k+1…j]s[k+1 \dots j]s[k+1j]

假设左部分可以由非终结符 BBB 推导出,右部分可以由非终结符 CCC 推导出,即 B∈dp[i][k]B \in dp[i][k]Bdp[i][k]C∈dp[k+1][j]C \in dp[k+1][j]Cdp[k+1][j]
那么,如果存在产生式 A→BCA \to BCABC ,则 AAA 就可以推导出整个子串 s[i…j]s[i \dots j]s[ij]
因此,将所有这样的 AAA 加入 dp[i][j]dp[i][j]dp[i][j] 中。

3. 判断结果

最后,检查根符号 SSS 是否在 dp[0][n−1]dp[0][n-1]dp[0][n1] 中。
如果在,说明整个字符串 sss 可以由根符号推导出,即 s∈L(G)s \in L(G)sL(G) ;否则, s∉L(G)s \notin L(G)s/L(G)


复杂度分析

  • 时间复杂度:O(n3⋅∣P∣)O(n^3 \cdot |P|)O(n3P) ,其中 nnn 是字符串长度, ∣P∣|P|P 是产生式数量。由于 n≤50n \le 50n50 ,该复杂度是可接受的。
  • 空间复杂度:O(n2⋅∣V∣)O(n^2 \cdot |V|)O(n2V) ,用于存储 dpdpdp 表。

实现细节

1. 输入处理

输入格式比较固定,但需要注意:

  • 每个测试用例以根符号开始。
  • 产生式规则以 # -> # 结束。
  • 候选字符串列表以 # 单独一行结束。
  • 输入中可能有空行,需要适当处理。

2. 数据结构设计

  • 使用 map<char, vector<string>> toTerminal 存储 A→aA \to aAa 类型的产生式,便于根据终结符查找非终结符。
  • 使用 map<string, vector<char>> toNonTerminal 存储 A→BCA \to BCABC 类型的产生式,键为字符串 “BCBCBC” ,值为可以推导出它的非终结符列表。
  • 使用 set<char> 存储 dp[i][j]dp[i][j]dp[i][j] ,避免重复。

3. 算法步骤

  1. 读取根符号、非终结符集、终结符集。
  2. 读取产生式,直到遇到 # -> # ,并分类存储。
  3. 对于每个候选字符串:
    • 初始化 dpdpdp 表(长度 1)。
    • 按长度递增动态规划填充 dpdpdp 表。
    • 判断根符号是否在 dp[0][n−1]dp[0][n-1]dp[0][n1] 中,输出结果。
  4. 每个测试用例后输出一个空行。

代码实现

// Right Words
// UVa ID: 10597
// Verdict: Accepted
// Submission Date: 2026-01-30
// UVa Run Time: 0.000s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

int main() {
    string root;
    while (cin >> root) {
        string V, T;
        cin >> V >> T;

        // 存储产生式
        vector<pair<char, string>> productions;
        string line;
        // 读取产生式,直到遇到 # -> #
        while (getline(cin, line)) {
            if (line == "# -> #") break;
            if (line.empty()) continue;
            // 解析产生式,格式如 "A -> BC" 或 "A -> a"
            char left = line[0];
            string right = line.substr(5);
            productions.push_back({left, right});
        }

        // 预处理:将产生式分类存储
        map<char, vector<string>> toTerminal;   // A -> a
        map<string, vector<char>> toNonTerminal; // A -> BC 映射为 BC -> A

        for (auto& prod : productions) {
            char left = prod.first;
            string right = prod.second;
            if (right.size() == 1) {
                // A -> a
                toTerminal[left].push_back(right);
            } else {
                // A -> BC
                toNonTerminal[right].push_back(left);
            }
        }

        // 处理候选字符串
        string candidate;
        while (getline(cin, candidate)) {
            if (candidate == "#") break;
            if (candidate.empty()) continue;

            int n = candidate.size();
            vector<vector<set<char>>> dp(n, vector<set<char>>(n));

            // 初始化:长度为 1 的子串
            for (int i = 0; i < n; i++) {
                char ch = candidate[i];
                for (char nt : V) {
                    if (toTerminal.count(nt)) {
                        for (string& term : toTerminal[nt]) {
                            if (term[0] == ch) {
                                dp[i][i].insert(nt);
                                break;
                            }
                        }
                    }
                }
            }

            // 动态规划:长度从 2 到 n
            for (int len = 2; len <= n; len++) {
                for (int i = 0; i <= n - len; i++) {
                    int j = i + len - 1;
                    for (int k = i; k < j; k++) {
                        // 检查所有可能的 B 和 C 组合
                        for (char B : dp[i][k]) {
                            for (char C : dp[k + 1][j]) {
                                string bc = string(1, B) + C;
                                if (toNonTerminal.count(bc)) {
                                    for (char A : toNonTerminal[bc]) {
                                        dp[i][j].insert(A);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // 判断根符号是否在 dp[0][n-1] 中
            if (dp[0][n - 1].count(root[0]))
                cout << candidate << " is in L(G)" << endl;
            else
                cout << candidate << " is not in L(G)" << endl;
        }
        cout << endl;
    }
    return 0;
}

更多推荐