Equivalenza tra PDA e Grammatiche Context Free (CFG)

Traduzione non autorizzata delle pagine del capitolo 2.2 di Sipser. In blu, i miei commenti.

In questa sezione mostriamo che le grammatiche context-free e gli automi pushdown sono equivalenti nelle loro potenzialità. Entrambi sono in grado di descrivere la classe dei linguaggi context-free. Mostriamo come convertire ogni grammatica context-free in un automa pushdown che riconosce lo stesso linguaggio e viceversa. Ricordiamo che abbiamo definito un linguaggio context-free come un linguaggio che può essere descritto con una grammatica context-free, il nostro obiettivo nel teorema seguente.

2.20 Teorema

Un linguaggio è context-free se e solo se un automa pushdown lo riconosce.

Come sempre, quando c'è un "se e solo se" in un teoriema, abbiamo due direzioni da provare. In questo teorema entrambe le direzioni sono interessanti (=incasinate). Prima, facciamo la direzione "se", più facile.

2.21 Lemma

Se un linguaggio è context-free, allora un automa pushdown lo riconosce.

Idea

Sia A un CFL. Dalla definizione, sappiamo che A ha una CFG, G, che lo genera. Mostriamo come convertire G in un PDA equivalente, che chiamiamo P.

Il PDA P che ora descriviamo accetta i suoi input w, se G genera questo input, determinando se c'è una derivazione per w. Ricordiamo che una derivazione è semplicemente la sequenza di sostituzioni fatte allo stesso modo in cui una grammatica genera una stringa. Ogni passo della derivazione produce una stringa intermedia di variabili e terminali. Disegnamo P per determinare se qualche serie di sostituzioni, usando le regole di G, possono condurre dalla variabile di partenza a w.

Una delle difficoltà nel testare se c'è una derivazione per w è immaginare quali sostituzioni fare. Il nondeterminismo del PDA permette di indovinare la sequenza di sostituzioni corrette. A ogni passaggio della derivazione, una delle regole per una variabile particolare è selezionata nondeterministicamente e usata per sostituire questa variabile.

Il PDA P inizia scrivendo la variabile di partenza nel suo stack. Passa attraverso una serie di stringhe intermedie, facendo una sostituzione dopo l'altra. Eventualmente può arrivare a una stringa che contiene solo simboli terminali, che significa che ha usato la grammatica per derivare la stringa (bingo!). Allora P accetta se la stringa è identica a quella che ha ricevuto in input.

Implementare questa strategia su un PDA richiede un'idea addizionale. Abbiamo bisogno di vedere come il PDA memorizza la stringa intermedia per andare da una a un'altra. Usare semplicemente lo stack per memorizzare ogni stringa intermedia è una tentazione. Però questo non funzione bene, perché il PDA ha bisogno di trovare la variabile nella stringa intermedia e fare sostituzioni. Il PDA può accedere solo al simbolo in cima allo stack e che può essere un simbolo terminale dentro una variabile. Il modo per aggirare il poblema è tenere solo una parte della stringa intermedia nello stack: i simboli con cui inizia la prima variabile nella stringa intermedia. Ogni simbolo terminale che appare prima della prima variabile è messa in corrispondenza immediatamente con i simboli nella stringa di input. La figura seguente illustra il PDA P.

Figura 2.22: P rappresenta la stringa intermedia 01A1A0

Di seguito, una descrizione informale di P.

  1. Metti il simbolo marcatore $ e la variabile di partenza nello stack
  2. Ripeti i seguenti passi all'infinito
    1. se la cima dello stack è una variabile simbolo A scegliere nondeterministicamente una delle regole per A e sostituire A con la stringa sul lato destro della regola
    2. se la cima dello stack è il simbolo terminale a, leggere il simbolo successivo dall'input e confrontarlo con a. Se è lo stesso, ripetere. Se non è lo stesso, rigettare questo ramo del nondeterminismo
    3. se la cima dello stack è il simbolo terminale $, entrare nello stato accettato. Facendo così, si accetta l'input se è stato letto interamente.

Qui manca un esempio, perché non si capisce molto.

Immaginiamo un PDA fatto così:

S → aAb

A → # | aAa

La stringa aa#ab è riconosciuta? Servono alcuni passaggi: aaAab (A = #), aAb (A = aAa), S (S = aAb), per cui va mantenuta la stringa intermedia.

  1. metto nello stack il simbolo di dollaro $ seguito da aAb, cioè la variabile di partenza, esclusi i terminali a destra. Il mio stack contiene $aA, e siccome è una LIFO, estraggo nell'ordine Aa$
  2. la cima dello stack è una variabile, quindi nondeterministicamente sostituisco A con # e aAa in due diversi branch
    1. # ⇒ nello stack ho #a$, ma quando confronto a#b con la mia stringa, ottengo una differenza, poiché il secondo carattere non è # ma a
    2. aAa => nello stack ho aAaa$, quindi la mia stringa completa è aaAab, ma quando vado a ricontrollarla sostituisco nondeterministicamente A con # e con aAa
      1. # ⇒ ottengo aa#ab, che è la mia stringa
      2. aAa ⇒ scarto questa opzione perché otterrei aaaAaab, ma il terzo carattere è # e non a.

Non ho capito ancora in che modo funziona lo scarico/carico dello stack.

Prova

Ora diamo un dettaglio formale della costruzione dell'automa pushdown P = (Q, Σ, Γ, δ, q1, F) . Per fare una costruzione più chiara, usiamo l'abbreviazione per la funzione di transizione. QUesta notazione fornisce un modo per scrivere una stringa intera nello stack in un passaggio della macchina. Possiamo simulare questa azione introducendo degli stati addizionali per scrivere la stringa un simbolo alla volta, come implementato nella seguente costruzione formale.

Siano q e r stati del PDA e sia a in Σε e s sia in Γε. Vogliamo che il PDA vada da q a r e legge a ed estrae s. Inoltre vogliamo che allo stesso tempo si spinga la stringa intera u = u1...ul nello stack. Possiamo implementare questa azione introducendo dei nuovi stati q1, ..., ql-1 e impostando la funzione di transizione in questo modo:

δ(q, a, s) per contenere (q1, ul),
δ(q1, ε, ε) = {(q2, ul-1)},
δ(q2, ε, ε) = {(q3, ul-2)},
δ(q3, ε, ε) = {(q4, ul-2)},
...
δ(ql-1, ε, ε) = {(r, u1)}.

Usiamo la notazione (r, u) ∈ δ(q, a ,s) per dire che quando q è lo stato dell'automa, a è il prossimo simbolo di input, s è il simbolo in cima allo stack, il PDA può leggere la a e prelevare la s, allora inserisce la stringa u nello stack e va allo stato r. La figura seguente mostra questa implementazione:

Figura 2.23: implementazione dell'abbreviazione (r, xyz) ∈ δ(q, a, s)

Gli stati di P sono Q = {qstart, qloop, qaccept} ∪ E, dove E è l'insieme di stati di cui abbiamo bisogno per implementare la scorciatoria appena descritta. Lo stato di inizio è qstart. L'unico stato accettato è qaccept.

La funzione di transizione è definita di seguito. Cominciamo inizializzando lo stack perché contenga i simboli $ e S, implementando il passo 1 nella descrizione informale: δ(qstart, ε, ε) = {(qloop, S$)}. Poi facciamo le transizioni per il ciclo principale del passo 2.

Prima ci occupiamo del caso (a), se la cima dello stack contiene una variabile. Sia δ(qloop, ε, A) = {(qloop, w) | dove A → w è una regola in R}.

Secondo, gestiamo il caso (b), se la cima dello stack contiene un terminale. Si ha che δ(qloop, a, a) = {(qloop, ε)}.

Infine, gestiamo il caso (c), se il marcatore di stack vuoto $ è in cima allo stack. Si ha che δ(qloop, ε, $) = {(qaccept, ε)}.

Il diagramma di stato è mostrato nella figura 2.24.

Figura 2.24: il diagramma di stato di P

Questo completa la prova del lemma 2.21.

La seconda parte la farò se e quando lo riterrò utile

Suggerimenti per il pumping lemma

Ieri siamo andati a parlare con il prof Calzavara e abbiamo raccolto un'importante indicazione sull'applicazione del pumping lemma.

  1. Non fare l'errore di applicare il PL a un esempio, "dimenticandosi" di includere la lunghezza p nella dimostrazione.

Per esempio, la soluzione di 1.49/b non può essere di prendere una stringa 1111010101... perché presuppone che p≤4, ma non prova niente nel caso in cui p>4.

Un'idea di fondo più "solida" sarebbe dire che abbiamo una stringa 1p01p che sicuramente è più lunga di p, e la cui parte sinistra sia xy, mentre z è la parte a destra dello zero. Facendo il pumping up si arriva ad avere un numero di 1 a sinistra maggiore del numero di 1 a destra, che contraddice la definizione del linguaggio.

  1. Il costrutto contrario di "per tutti esiste uno" è "per qualcuno non esiste nessuno".

Non è sufficiente trovare un caso in cui qualcosa non funziona, ma bisogna trovare un caso in cui niente funziona.

  1. In ogni caso, quando si parla di pumping lemma più che dividere la stringa in tre parti è meglio valutare se dividerla in una "parte sinistra" pompabile e una parte destra.

Più che cercare di definire quale sia la suddivisione in xyz, è meglio definire le caratteristiche che dovrebbe avere questa suddivisione.

  1. Il pumping lemma nei linguaggi context-free è più complicato, perché non c'è più una "parte sinistra" pompabile, ma c'è una suddivisione in cinque della stringa per cui la zona pompabile diventa quella centrale.

exercise 1.29

Use the pumping lemma to show that the following languages are not regular.
a. A1 = {0n1n2n| n ≥ 0}
b. A2 = {www | w ∈ {a, b}}
c. A3 = {a2n| n ≥ 0} (Here, a2n means a string of 2n a’s.)

a. Theorem

A1 = {0n1n2n| n ≥ 0} is not regular.

a. Proof

Applying the pumping lemma to a string that matches with A1 we can have two scenarios:

  1. y is composed by only 0s or 1s or 2s. For example, 012 with y = 1. When we change the number of ys, we obtain a different number of 1s than others: 02, 0112, 01112, ..., and this doesn't match with A1 anymore
  2. y is composed by a pair or more of numbers. For example, 001122, with y = 0011. Changing the number of ys, the order of the numbers are not kept: 22, 0011001122, 00110011001122...

so A1 is not regular.

b. Theorem

A2 = {www | w ∈ {a, b}} is not regular.

b. Proof

We show that choosing a pattern w, composed by as and bs, the language A2, that recognizes the pattern w repeated three times, is not regular.

If the y part of the pumping lemma is composed by a substring of w, repeat the y more times means don't match A2 anymore, because the string can be composed by other than ws. I.E. w = aab, a valid string is aabaabaab, y = the central pair aa: repeating y, we obtain: aabbaab (0 times), aabaabaab (matches), aabaaaabaab (1 time). The first and the third examples are not composed by ws.

If the y part of the pumping lemma is exactly w, repeat the y a number of times different by 1 mod 3 (1, 4, 7, 10...) doesn't match the rule of three times w. I.E. w = aab, y = aab ⇒ aabaab (0 times, doesn't match); aabaabaab (1 time, matches), aabaabaabaab (2 times, doesn't match)...

So, A2 is not regular.

c. Theorem

A3 = {a2n| n ≥ 0} (Here, a2n means a string of 2n a’s.)

c. Proof

A3 recognizes a, aa, aaaa, aaaaaaaa, ...

Choose x = a, y = aa, z = a seems a good idea, because we're sure that every string recognized by A3 is writable with an xyiz, but there are two problems: a single "a" is not writable with this pattern, and there are strings that are writable but don't be recognized by A3, for example, i = 2 ⇒ a aa aa a (the spaces have been added to make the string more readable).

Choosing a simpler definition of x, y, z, as x = ε, y = a, z = ε, we are affected by the same problem.

exercise 1.49

a. Theorem

Let B = {1ky|y∈{0,1}* and y contains at least k 1s, for k ≥ 1}. Show that B is a regular language.

a. Proof

B equals to "Let B is a string that begins with 1 and, after, contains at least another 1".

This DFA doesn't "count" the number of 1s

b. Theorem

Let B = {1ky|y∈{0,1}* and y contains at most k 1s, for k ≥ 1}. Show that B is not a regular language.

b. Proof

As shown in 1.77 Sipser's example, we can apply an absurd proof of the pumping lemma to this language

Let we assume that B is regular. Let we choose one 1 as pumping's y and the second part (i.e. from the first 0 found) as pumping's z, and it seems to work:

1111y0100z

or better

1y11y21y31yn0100z

Pumping lemma doesn't allows a length of 0 for the y part, but allows that y is repeated zero times.

Using as example the previous string, with y repeated zero times, we obtain the string 0100, that doesn't match. Thus we obtain a contradiction.