Postać normalna Chomsky'ego, algorytm Cocke-Younger'a-Kasami

Wstęp

W tym wykładzie zajmiemy się problemem jak efektywnie rozpoznawać, czy dane słowo należy do języka generowanego przez daną gramatykę. W tym celu poznamy najpierw szczególną postać gramatyk bezkontekstowych, tzw. postać normalną Chomsky'ego i dowiemy się jak przekształcać gramatyki do tej postaci. Następnie poznamy dynamiczny algorytm rozpoznający czy dane słowo można wyprowadzić w danej gramatyce w postaci normalnej Chomsky'ego.

Postać normalna Chomsky'ego

Definicja

Powiemy, że gramatyka jest w postaci normalnej Chomsky'ego, jeśli wszystkie produkcje w gramatyce są postaci: $A \to BC$ lub $A \to a$ (dla pewnych nieterminali A, B i C oraz terminala a).
Zauważmy, że jeśli gramatyka jest w postaci normalnej Chomsky'ego, to każda produkcja przekształca jeden nieterminal na dwa nieterminale, lub jeden nieterminal na jeden nieterminal. Ułatwia to rozstrzygnięcie, czy dane słowo x da się wyprowadzić w danej gramatyce -- wiadomo, że w takim wyprowadzeniu |x|-1 razy należy zastosować produkcje przekształcające nieterminal na dwa nieterminale i |x| razy zastosować produkcje przekształcające nieterminal na terminal.

Podstawowe pytanie, jakie można sobie zadać brzmi: czy dla każdego języka bezkontekstowego istnieje generująca go gramatyka w postaci normalnej Chomsky'ego? Odpowiedź brzmi: nie. Zauważmy, że zastosowanie dowolnej produkcji nie zmniejsza liczby symboli. Ponieważ zaczynamy od słowa złożonego z jednego symbolu (aksjomatu), więc nigdy nie uzyskamy słowa pustego. Jeśli więc język A jest bezkontekstowy i $\varepsilon \in A$, to A nie jest generowany przez żadną gramatykę w postaci normalnej Chomsky'ego.

Na szczęście słowo puste jest jedynym elementem, którego nie możemy uzyskać rozważając gramatyki w postaci normalnej Chomsky'ego. Jeżeli dopuścimy, aby słowo puste zniknęło z języka generowanego przez gramatykę, to każdą gramatykę można przekształcić do postaci normalnej Chomsky'ego.


\begin{fact}
Dla dowolnej gramatyki bezkontekstowej $G$\ istnieje taka
gramaty...
...ej
Chomsky'ego $G'$, że
$L(G') = L(G) \setminus \{\varepsilon\}$.
\end{fact}

Dowód:

Niech $G=\angles{N, \Sigma, P, S}$. Konstrukcja gramatyki G' przebiega w kilku krokach:
  1. Niech gramatyka $G_1=\angles{N, \Sigma, P_1, S}$ będzie gramatyką powstałą z G przez domknięcie zbioru produkcji zgodnie z następującymi dwiema regułami: Stosowanie powyższych reguł powtarzamy tak długo, jak długo wprowadzają one jakiekolwiek nowe produkcje. Ponieważ dodawane produkcje nie są dłuższe od już istniejących, więc cały proces doda tylko ograniczoną liczbę produkcji i w pewnym momencie się zakończy.

    Zauważmy, że L(G1) = L(G), gdyż dodawane produkcje nie pozwalają wyprowadzić niczego, czego i tak nie dałoby się wcześniej wyprowadzić.

  2. Dla dowolnego słowa z $x \in L(G_1)$, $x \neq \varepsilon$ rozważymy jego najkrótsze wyprowadzenie. W takim wyprowadzeniu nie będą się pojawiały produkcje postaci $X \to \varepsilon$ ani $X \to Y$. Niech G2 będzie gramatyką powstałą z G1 przez usunięcie tych produkcji, $G_2 = \angles{N, \Sigma, P_2, S}$, gdzie

    \begin{displaymath}
P_2 = P_1 \setminus \{X \to \varepsilon, X \to Y : X,Y \in N\}
\end{displaymath}

    Wówczas:

    \begin{displaymath}
L(G_2) =
L(G_1) \setminus \{\varepsilon\} =
L(G) \setminus \{\varepsilon\}
\end{displaymath}

  3. Gramatyka G3 powstaje z G2 przez dodanie do niej, dla każdego terminala $a, b, c, \dots \in N$, odpowiadającego mu nowego nieterminala ( $A, B, C, \dots$), oraz produkcji: $A \to a$, $B \to b$, $C \to c$, .... We wszystkich pozostałych produkcjach $X \to \alpha$, gdzie $\vert\alpha\vert > 1$, zamieniamy wszystkie terminale na odpowiadające im nieterminale. Nie zmienia to języka generowanego przez gramatykę:

    \begin{displaymath}
L(G_3) =
L(G_2) =
L(G) \setminus \{\varepsilon\}
\end{displaymath}

    Natomiast w ten sposób w G3 mamy tylko dwa rodzaje produkcji: $X \to a$ gdzie a jest terminalem, oraz $X \to Y_1 Y_2 \dots Y_k$ gdzie Yi to nieterminale i k>1.
  4. Gramatyka G4 powstaje z G3 przez zastąpienie każdej produkcji postaci $X \to Y_1 Y_2 \dots Y_k$ (dla k>2) przez zestaw produkcji:

    \begin{eqnarray*}
X &\to& Y_1 X_1\\
X_1 &\to& Y_2 X_2\\
&\vdots&\\
X_{k-2} &\to& Y_{k-1} Y_k
\end{eqnarray*}


    gdzie $X_1, X_2, \dots, X_{k-2}$ to nowe nieterminale dodane do G4. Tak uzyskana gramatyka generuje dokładnie ten sam język -- jedyne co się zmieniło to to, że zamiast zastosowania jednej produkcji, trzeba ich zastosować kilka,

    \begin{displaymath}
L(G_4) =
L(G_3) =
L(G) \setminus \{\varepsilon\}
\end{displaymath}

    Natomiast G4 jest już w postaci normalnej Chomsky'ego. \qed

Przykład

Zobaczmy, jak powyższy algorytm działa na przykładzie gramatyki generującej język $\{a^nb^n: n \ge 0\}$.

\begin{displaymath}
S \to aSb \;\vert\;\varepsilon
\end{displaymath}

Pierwszy krok powoduje dodanie produkcji $S \to ab$:

\begin{displaymath}
S \to aSb \;\vert\;ab \;\vert\;\varepsilon
\end{displaymath}

W drugim kroku usuwamy produkcję $S \to \varepsilon$.

\begin{displaymath}
S \to aSb \;\vert\;ab
\end{displaymath}

W trzecim kroku dodajemy nieterminale A i B, zastępujemy nimi w istniejących produkcjach terminale a i b, oraz dodajemy produkcje $A \to a$ i $B \to b$.

\begin{eqnarray*}
S &\to& ASB \;\vert\;AB\\
A &\to& a\\
B &\to& b
\end{eqnarray*}


W ostatnim kroku algorytmu rozbijamy produkcję $S \to ASB$ na dwie produkcje.

\begin{eqnarray*}
S &\to& AX \;\vert\;AB\\
X &\to& SB\\
A &\to& a\\
B &\to& b
\end{eqnarray*}


Tak uzyskana gramatyka jest w postaci normalnej Chomsky'ego. Na przykład, wyprowadzenie słowa aaabbb ma postać:

\begin{eqnarray*}
S &\to& AX \to ASB \to aSB \to aSb \to \\
&\to& aAXb \to aA...
...\to aaSBb \to aaSbb \to \\
&\to& baABbb \to aaaBbb \to aaabbb
\end{eqnarray*}


Przykład

Zobaczmy, jak powyższy algorytm działa na przykładzie gramatyki generującej wyrażenia nawiasowe.

\begin{displaymath}
S \to [S] \;\vert\;SS \;\vert\;\varepsilon
\end{displaymath}

Pierwszy krok powoduje dodanie produkcji: $S \to []$ i $S \to S$:

\begin{displaymath}
S \to [S] \;\vert\;[] \;\vert\;SS \;\vert\;S \;\vert\;\varepsilon
\end{displaymath}

W drugim kroku usuwamy produkcje $S \to \varepsilon$ i $S \to S$.

\begin{displaymath}
S \to [S] \;\vert\;[] \;\vert\;SS
\end{displaymath}

W trzecim kroku dodajemy nieterminale L i P, zastępujemy nimi w istniejących produkcjach odpowiednio terminale [ i ], oraz dodajemy produkcje $L \to [$ i $P \to ]$.

\begin{eqnarray*}
S &\to& LSP \;\vert\;LP \;\vert\;SS\\
L &\to& [\\
P &\to& ]
\end{eqnarray*}


W ostatnim kroku algorytmu rozbijamy produkcję $S \to LSP$ na dwie produkcje.

\begin{eqnarray*}
S &\to& LX \;\vert\;LP \;\vert\;SS\\
X &\to& SP\\
L &\to& [\\
P &\to& ]
\end{eqnarray*}


Tak uzyskana gramatyka jest w postaci normalnej Chomsky'ego. Na przykład, wyprowadzenie słowa [[][[]]] w tej gramatyce ma postać:

\begin{eqnarray*}
S &\to& LX \to LSP \to [SP \to [S] \to \\
&\to& [SS] \to [L...
...] \to [[][S]] \to \\
&\to& [[][LP]] \to [[][[P]] \to [[][[]]]
\end{eqnarray*}


Algorytm rozpoznawania Cocke-Younger'a-Kasami (CYK)

Algorytm CYK pozwala stwierdzić, czy dane słowo jest wyprowadzalne w danej gramatyce (w postaci normalnej Chomsky'ego). Jeśli gramatyka nie jest w postaci normalnej Chomsky'ego, to można ją sprowadzić do tej postaci zgodnie z opisaną wyżej konstrukcją.

Tak jak to zwykle bywa w programowaniu dynamicznym, żeby rozwiązać dany problem musimy go najpierw uogólnić i zamiast szukać odpowiedzi na jedno pytanie, znajdziemy odpowiedzi na całe mnóstwo powiązanych ze sobą pytań.

Niech $G=\angles{N, \Sigma, P, S}$ będzie daną gramatyką w postaci normalnej Chomsky'ego, a dane słowo $x \in \Sigma^*$ będzie postaci $x = a_1 a_2 \dots a_n$. Dla każdej pary indeksów $1 \le i \le j \le n$ wyznaczymy wszystkie takie nieterminale X, z których można wyprowadzić podsłowo $a_i \dots a_j$. Dokładniej, wypełnimy tablicę:

\begin{displaymath}
T[i,j] = \{ X \in N : X \to^* a_i \dots a_j \}
\end{displaymath}

Gdy już będziemy mieli obliczoną tablicę T, to słowo x można wyprowadzić w gramatyce G wtw., gdy $S \in T[1,n]$.

W jaki sposób możemy wypełniać tę tablicę?

Tablica ma rozmiar $\Theta(n^2)$ przy czym aby wypełnić jeden element tablicy trzeba wykonać liniową liczbę operacji $\otimes$, tak więc złożoność czasowa tego algorytmu to $\Theta(n^3)$.

Przykład

Rozważmy gramatykę w postaci normalnej Chomsky'ego:

\begin{eqnarray*}
S &\to& SS \;\vert\;AB \\
A &\to& AS \;\vert\;AA \;\vert\;a\\
B &\to& SB \;\vert\;BB \;\vert\;b
\end{eqnarray*}


oraz słowo aabbab. Algorytm CYK da nam w wyniku następującą tablicę T:

\begin{displaymath}
\begin{array}{ccccccc}
& 1 & 2 & 3 & 4 & 5 & 6 \\ \cline{2...
...
\multicolumn{1}{\vert c\vert}{B} \\ \cline{7-7}
\end{array} \end{displaymath}

Jeżeli spróbujemy odtworzyć skąd w T[1,6] wzięło się S, to uzyskamy informacje o drzewach wyprowadzenia badanego słowa. Poniżej podano przykładowe drzewo wyprowadzenia i zaznaczono odpowiadające mu nieterminale w tablicy T.

\begin{displaymath}
\begin{array}{ccccccc}
& 1 & 2 & 3 & 4 & 5 & 6 \\ \cline{2...
...olumn{1}{\vert c\vert}{\mathbf{B}} \\ \cline{7-7}
\end{array} \end{displaymath}


\begin{displaymath}
\xymatrix{
& & & S \ar@{-}[dll] \ar@{-}[drr] \\
& S \ar@...
...a & & b \\
A \ar@{-}[d] & & B \ar@{-}[d] & b\\
a & & b
}
\end{displaymath}

Podsumowanie

W tym wykładzie poznaliśmy postać normalną Chomsky'ego gramatyk bezkontekstowych i pokazaliśmy, że każdą gramatykę bezkontekstową można sprowadzić do tej postaci (z dokładnością do akceptowania słowa pustego $\varepsilon$). Poznaliśmy też algorytm CKY rozpoznający, czy dane słowo należy do języka generowanego przez daną gramatykę bezkontekstową w postaci normalnej Chomsky'ego.

Skorowidz

Praca domowa

  1. Sprowadź do postaci normalnej Chomsky'ego następującą gramatykę bezkontekstową:

    \begin{eqnarray*}
S &\to& ab \;\vert\;ba \;\vert\;T \\
T &\to& SS \;\vert\;aSb \;\vert\;bSa
\end{eqnarray*}


  2. Dla podanej gramatyki bezkontekstowych w postaci normalnej Chomsky'ego i słowa abaabb zasymuluj działanie algorytmu CYK. Podaj zawartość tablicy T oraz przykładowe drzewo wyprowadzenia.

    \begin{eqnarray*}
S &\to& SS \;\vert\;AB\\
A &\to& AA \;\vert\;a\\
B &\to& BB \;\vert\;b
\end{eqnarray*}


Ćwiczenia

  1. Sprowadź do postaci normalnej Chomsky'ego następujące gramatyki bezkontekstowe:
  2. Dla każdej z poniższych gramatyk bezkontekstowych w postaci normalnej Chomsky'ego zasymuluj działanie algorytmu CYK dla podanego słowa. Czy podane słowo faktycznie można wyprowadzić w podanej gramatyce? Jeśli tak, to podaj przykładowe drzewo wyprowadzenia.