We can implement probabilistic CYK (PCYK) for a CNF PCFG using dynamic programming over spans: for each span (i, j) store best probability for every nonterminal A that can derive substring words[i:j]. We use log-probabilities to avoid underflow and keep O(n^3 |G|) by iterating span length, split point, and binary rules A -> B C. Chart parsing means filling a triangular chart of span cells; each cell holds a map from nonterminal -> (best_log_prob, backpointer).Approach:1. Preprocess grammar into CNF with log probabilities and index binary rules by (B,C) -> list of (A, logP).2. Initialize length-1 spans using terminal rules A -> word with unary conversions in CNF.3. For span length L from 2..n, for i=0..n-L, j=i+L: for split k in (i+1..j-1), for each B in chart[i,k], C in chart[k,j], for each A in rules[(B,C)]: compute score = logP(A->B C) + score(B) + score(C); keep max for A.4. Final parse = best A covering (0,n) for start symbol.Example implementation (simplified, returns best parse tree):python
import math
from collections import defaultdict
LOG0 = -1e300
def log(x): return math.log(x) if x>0 else LOG0
# grammar: list of (lhs, rhs_tuple, prob)
# rhs_tuple length 1 for terminals, 2 for binaries
def pcfg_cyk(words, grammar, start='S'):
n = len(words)
# index rules
bin_index = defaultdict(list) # (B,C) -> [(A,logp)]
term_index = defaultdict(list) # word -> [(A,logp)]
for A, rhs, p in grammar:
lp = log(p)
if len(rhs)==1:
term_index[rhs[0]].append((A, lp))
else:
bin_index[(rhs[0], rhs[1])].append((A, lp))
chart = [ [dict() for _ in range(n+1)] for _ in range(n) ]
back = [ [dict() for _ in range(n+1)] for _ in range(n) ]
# length-1
for i,w in enumerate(words):
for A, lp in term_index.get(w,[]):
chart[i][i+1][A] = lp
back[i][i+1][A] = (w,)
# DP
for L in range(2, n+1):
for i in range(0, n-L+1):
j = i+L
cell = chart[i][j]
for k in range(i+1, j):
left = chart[i][k]
right = chart[k][j]
for B, lb in left.items():
for C, lc in right.items():
for A, lap in bin_index.get((B,C),[]):
score = lap + lb + lc
if score > cell.get(A, LOG0):
cell[A] = score
back[i][j][A] = (k, B, C)
# reconstruct
if start not in chart[0][n]:
return None
def build(i,j,A):
bp = back[i][j][A]
if len(bp)==1:
return (A, bp[0])
k,B,C = bp
return (A, build(i,k,B), build(k,j,C))
return build(0,n,start)
Key concepts:- Chart parsing stores partial derivations per span enabling reuse (dynamic programming).- Using log-probabilities converts products to sums; keep max (Viterbi parse).Complexity:- Time: O(n^3 * |R_bin|) but effectively O(n^3 * |G|) since for each span and split we examine pairs and rule lookups.- Space: O(n^2 * |N|) for chart entries.Scaling techniques:- Pruning: discard low-probability entries per cell below a threshold relative to cell max (beam-like) to reduce pairings.- Beam search: keep top-K nonterminals per cell (chart cell beam); reduces combinations from |N|^2 to K^2 per split.- Packed representations: use shared packed forest (compact hypergraph) where identical subtrees share structure — store alternative derivations compactly rather than single backpointer; required for n-best or full posterior marginals (inside-outside).- Additional: use unary closure precomputation, sparse indexing (only iterate over present B/C), and parallelization over splits.Edge cases:- Unknown words: add OOV rules or smoothing.- Non-CNF grammars: convert to CNF carefully preserving probabilities.This implementation gives the standard O(n^3 |G|) Viterbi-CYK and the listed scaling techniques are practical ways to parse longer sentences in real systems.