Programação dinâmica em linguagens

Reconhecimento CYK e construção de Kleene, com estados por intervalos e por intermediários permitidos.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

As regras S→ABS\to AB, A→aA\to a e B→bB\to b geram ab: começamos no símbolo inicial SS e substituímos os símbolos auxiliares até restarem terminais. Para palavras maiores, testar todas as derivações seria impraticável. O CYK combina os símbolos que geram segmentos menores numa tabela por intervalos e responde sem enumerar árvores.

A construção de Kleene também guarda resultados parciais, mas descreve caminhos de um autómato com expressões regulares. Os ciclos exigem representar repetições com estrela, em vez de enumerar todos os caminhos possíveis.

CYK e forma normal de Chomsky

CYK decide se uma palavra pertence à linguagem de uma gramática independente de contexto, ou seja, ao conjunto das palavras obtidas por substituições sucessivas a partir do símbolo inicial até restarem terminais. Usa produções da forma A→BCA\to BC ou A→aA\to a, com não-terminais A,B,CA,B,C e terminal aa. Estas regras são a forma normal de Chomsky para palavras não vazias. Pode admitir-se ainda S→εS\to\varepsilon, desde que o inicial SS não apareça em lados direitos; a palavra vazia é então tratada separadamente. Regras unitárias, como A→BA\to B, ou regras com três não-terminais exigem conversão antes deste algoritmo.

Define T[i,ℓ]T[i,\ell] como o conjunto de não-terminais que geram o segmento da palavra que começa no índice ii e tem comprimento ℓ\ell. Para comprimento 1, inclui AA quando há produção A→w[i]A\to w[i].

Para ℓ≥2\ell\ge2, experimenta cada divisão kk entre 1 e ℓ−1\ell-1. Inclui AA em T[i,ℓ]T[i,\ell] quando existe A→BCA\to BC com

B∈T[i,k]eC∈T[i+k,ℓ−k].B\in T[i,k]\quad\text{e}\quad C\in T[i+k,\ell-k].

Preenche comprimentos por ordem crescente. O símbolo inicial em T[0,n]T[0,n] significa aceitação. A regra combina os dois filhos da raiz de uma árvore de derivação; por indução no comprimento, os conjuntos representam exatamente os segmentos deriváveis.

Uma tabela pequena

Considera a gramática

S→AB∣AC,A→a,B→BA∣b,C→AA.S\to AB\mid AC,\qquad A\to a,\qquad B\to BA\mid b,\qquad C\to AA.

Para aba, a linha de comprimento 1 é {A},{B},{A}\{A\},\{B\},\{A\}. Para comprimento 2:

  • ab contém SS, porque S→ABS\to AB.
  • ba contém BB, porque B→BAB\to BA.

Para comprimento 3, dividir a | ba permite S→ABS\to AB. A divisão ab | a não encontra produção com os pares disponíveis S,AS,A. O conjunto final é {S}\{S\}; a palavra é aceite. Uma derivação é S⇒AB⇒aB⇒aBA⇒abA⇒abaS\Rightarrow AB\Rightarrow aB\Rightarrow aBA\Rightarrow abA\Rightarrow aba.

ComprimentoSegmento na posição 0Segmento na posição 1Segmento na posição 2
1a: {A}\{A\}b: {B}\{B\}a: {A}\{A\}
2ab: {S}\{S\}ba: {B}\{B\}Não existe
3aba: {S}\{S\}Não existeNão existe

As posições sem segmento não são conjuntos vazios: estão fora da tabela triangular. Para abb, o segmento final bb tem conjunto vazio, pois nenhuma produção combina B com B. A palavra inteira também fica sem não-terminais e é rejeitada.

O código mantém visível o preenchimento da tabela e a decisão de aceitação. mostrar_tabela_cyk apenas escreve os conjuntos por comprimento; podes consultar essa função no código de apoio. Troca a palavra de entrada por abb e compara as tabelas.

Ficheiros do exemplo
Tabela CYK
from cyk_apoio import mostrar_tabela_cyk

word = input().strip()
if not word or len(word) > 40 or any(c not in "ab" for c in word):
    print("entrada invalida")
else:
    terminal = {"a": {"A"}, "b": {"B"}}
    binary = [("S", "A", "B"), ("S", "A", "C"),
              ("B", "B", "A"), ("C", "A", "A")]
    n = len(word)
    table = {}
    for i, char in enumerate(word):
        table[i, 1] = set(terminal[char])
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            found = set()
            for split in range(1, length):
                for parent, left, right in binary:
                    if left in table[i, split] and right in table[i + split, length - split]:
                        found.add(parent)
            table[i, length] = found
    mostrar_tabela_cyk(table, n)
    print("aceite" if "S" in table[0, n] else "rejeitada")
def mostrar_tabela_cyk(table, n):
    for length in range(1, n + 1):
        cells = ["{" + ",".join(sorted(table[i, length])) + "}"
                 for i in range(n - length + 1)]
        print(f"comprimento {length}: " + " ".join(cells))
Resultado

Executa o código para ver a saída e os gráficos.

A saída lista as linhas de comprimento 1, 2 e 3 e termina com aceite. abb é rejeitada. O programa aceita apenas palavras não vazias de a e b, até 40 símbolos, e usa exatamente esta gramática.

Para uma gramática fixa, há O(n2)O(n^2) células e O(n)O(n) divisões por célula, dando O(n3)O(n^3) tempo. Se a gramática fizer parte da entrada e percorremos as suas produções binárias, o limite é O(n3∣P∣)O(n^3|P|). Os conjuntos usam O(n2∣N∣)O(n^2|N|) espaço na representação direta, onde NN é o conjunto de não-terminais.

Reconhecer não é contar derivações

Ter dois não-terminais numa célula não prova ambiguidade. Pode apenas significar que dois símbolos diferentes geram o mesmo segmento. A gramática é ambígua se alguma palavra tiver duas árvores de derivação distintas a partir do símbolo inicial.

Por exemplo, S→AAS\to AA, A→AA∣aA\to AA\mid a gera aaa com as divisões a | aa e aa | a na raiz. Para reconstruir uma árvore, guarda uma produção e uma divisão por inclusão. Para investigar ambiguidade desta palavra, guarda todas as alternativas ou conta árvores por estado. Apenas os conjuntos de CYK não mostram quantas alternativas levaram ao mesmo não-terminal.

Operações regulares antes de Kleene

Uma expressão regular combina linguagens com três operações. A união R∣SR\mid S aceita palavras de qualquer das partes. A concatenação RSRS cola uma palavra de RR com outra de SS. A estrela R∗R^* repete RR zero ou mais vezes. O símbolo ∅\varnothing representa o conjunto vazio; ε\varepsilon representa a palavra vazia de comprimento zero. Assim (ε∣a)(\varepsilon\mid a) aceita vazio ou aa, e (ε∣a)∗(\varepsilon\mid a)^* aceita qualquer sequência de aa, ou seja a∗a^*.

Construção de Kleene

Um autómato finito pode ter ciclos, portanto enumerar os seus caminhos não termina. A construção de Kleene descreve os caminhos através de expressões regulares, permitindo progressivamente mais estados intermédios.

Num autómato com estados 1,…,n1,\ldots,n, define Rij(k)R_{ij}^{(k)} como a expressão regular das palavras que levam de ii a jj usando apenas 1,…,k1,\ldots,k como estados intermédios. Inicialmente, Rij(0)R_{ij}^{(0)} é a união dos rótulos das transições diretas; inclui ε\varepsilon se i=ji=j. Se não há transição nem caminho vazio, usa ∅\varnothing.

Rij(k)=Rij(k−1)∣Rik(k−1)(Rkk(k−1))∗Rkj(k−1).R_{ij}^{(k)}=R_{ij}^{(k-1)}\mid R_{ik}^{(k-1)}\big(R_{kk}^{(k-1)}\big)^*R_{kj}^{(k-1)}.

A primeira alternativa não passa por kk. A segunda chega a kk, repete zero ou mais voltas de kk para kk e sai para jj. O asterisco é necessário: sem ele, só permitiríamos uma visita a kk.

A expressão final é a união de Rsf(n)R_{sf}^{(n)} para todos os estados finais ff, onde ss é o inicial. Se o inicial também for final, a palavra vazia pertence à linguagem.

Exemplo com dois estados

Se o estado 1 é inicial, 2 é final, há ciclo 1→a11\xrightarrow{a}1, transição 1→b21\xrightarrow{b}2 e ciclo 2→c22\xrightarrow{c}2, os estados iniciais são

R11(0)=ε∣a,R12(0)=b,R21(0)=∅,R22(0)=ε∣c.R_{11}^{(0)}=\varepsilon\mid a,\quad R_{12}^{(0)}=b,\quad R_{21}^{(0)}=\varnothing,\quad R_{22}^{(0)}=\varepsilon\mid c.

Permitir o intermediário 1 dá R12(1)=b∣(ε∣a)(ε∣a)∗bR_{12}^{(1)}=b\mid(\varepsilon\mid a)(\varepsilon\mid a)^*b, equivalente a a∗ba^*b. Permitir o intermediário 2 acrescenta repetições de cc, dando uma expressão equivalente a a∗bc∗a^*bc^*.

A palavra aaabcc pertence à linguagem; aaacc não, porque falta a transição bb que chega ao estado final. ∅\varnothing e ε\varepsilon são distintos: o primeiro não gera palavra nenhuma, o segundo gera uma palavra de comprimento zero.

Custo e semelhança com Floyd-Warshall

Há nn etapas e n2n^2 pares, logo O(n3)O(n^3) combinações de expressões. Isso não garante que o texto expandido da expressão final tenha tamanho polinomial. Copiar expressões inteiras pode provocar crescimento muito grande; partilhar subexpressões numa representação em grafo evita cópias, mas expandi-las para uma string pode continuar caro.

O índice exterior kk tem o mesmo papel que em Floyd-Warshall. Ali combinamos custo por mínimo e soma; aqui combinamos linguagens por união, concatenação e fecho. Esta semelhança vem do significado dos estados, não de coincidência de código. Para uma implementação direta de Kleene, usa os valores da etapa anterior ou uma atualização cuidadosamente justificada para não misturar níveis.

Exercícios

A célula não conta árvores

Na gramática S→AA e A→AA|a, usa CYK para aceitar aaa e mostra duas árvores diferentes. Explica por que guardar apenas conjuntos não revela o número de árvores.

Pista

Cada símbolo a é gerado por A e aa também é gerado por A.

Mais uma pista

Na raiz S→AA, divide a|aa ou aa|a.

Solução

A linha de comprimento 1 contém A nas três posições. Cada segmento aa contém A por A→AA e S por S→AA. Para aaa, S surge com divisão 1+2 e com divisão 2+1. Uma árvore tem o A esquerdo a derivar diretamente a por A→a e o direito com dois filhos; a outra inverte esses tamanhos. O conjunto final contém S uma única vez, embora haja duas formas de o inserir. É preciso guardar divisões/produções ou contagens para ver essa multiplicidade.

Confere a tua resposta:

  • Mostrei as divisões 1+2 e 2+1.
  • Distingui conjunto de contagem de derivações.
  • Identifiquei duas árvores a partir de S.

Erros frequentes

Dois símbolos diferentes numa célula, por si só, não provam ambiguidade do símbolo inicial. Duas ordens de expansão da mesma árvore também não são duas árvores distintas.

Voltar à explicação
Não esquecer repetições no estado intermédio

Um autómato tem inicial p, final r e transições p→q com a, q→q com b e q→r com c. Dá uma expressão regular e justifica a contribuição do ciclo quando q é permitido como intermediário.

Pista

O caminho chega a q com a e sai com c. Pode repetir o ciclo zero vezes.

Mais uma pista

Usa R[p,q] (R[q,q])* R[q,r].

Solução

A expressão é abc. A construção permite chegar a q, dar zero ou mais voltas pelo ciclo de rótulo b e sair por c. Como R[q,q] inclui também o caminho vazio, (ε|b) simplifica-se para b*. ac e abbbc são aceites; a ou abcabc não satisfazem esta sequência de transições.

Confere a tua resposta:

  • Obtive ab*c.
  • Expliquei zero ou mais repetições.
  • Distingui ε e ausência de caminho.

Erros frequentes

Escrever abc obriga exatamente uma volta, e escrever ab+c obriga pelo menos uma e rejeita ac.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.