Programação dinâmica em linguagens
Reconhecimento CYK e construção de Kleene, com estados por intervalos e por intermediários permitidos.
As regras , e geram ab: começamos no símbolo inicial 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 ou , com não-terminais e terminal . Estas regras são a forma normal de Chomsky para palavras não vazias. Pode admitir-se ainda , desde que o inicial não apareça em lados direitos; a palavra vazia é então tratada separadamente. Regras unitárias, como , ou regras com três não-terminais exigem conversão antes deste algoritmo.
Define como o conjunto de não-terminais que geram o segmento da palavra que começa no índice e tem comprimento . Para comprimento 1, inclui quando há produção .
Para , experimenta cada divisão entre 1 e . Inclui em quando existe com
Preenche comprimentos por ordem crescente. O símbolo inicial em 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
Para aba, a linha de comprimento 1 é . Para comprimento 2:
abcontém , porque .bacontém , porque .
Para comprimento 3, dividir a | ba permite . A divisão ab | a não encontra produção com os pares disponíveis . O conjunto final é ; a palavra é aceite. Uma derivação é .
| Comprimento | Segmento na posição 0 | Segmento na posição 1 | Segmento na posição 2 |
|---|---|---|---|
| 1 | a: | b: | a: |
| 2 | ab: | ba: | Não existe |
| 3 | aba: | Não existe | Nã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.
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))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á células e divisões por célula, dando tempo. Se a gramática fizer parte da entrada e percorremos as suas produções binárias, o limite é . Os conjuntos usam espaço na representação direta, onde é 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, , 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 aceita palavras de qualquer das partes. A concatenação cola uma palavra de com outra de . A estrela repete zero ou mais vezes. O símbolo representa o conjunto vazio; representa a palavra vazia de comprimento zero. Assim aceita vazio ou , e aceita qualquer sequência de , ou seja .
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 , define como a expressão regular das palavras que levam de a usando apenas como estados intermédios. Inicialmente, é a união dos rótulos das transições diretas; inclui se . Se não há transição nem caminho vazio, usa .
A primeira alternativa não passa por . A segunda chega a , repete zero ou mais voltas de para e sai para . O asterisco é necessário: sem ele, só permitiríamos uma visita a .
A expressão final é a união de para todos os estados finais , onde é 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 , transição e ciclo , os estados iniciais são
Permitir o intermediário 1 dá , equivalente a . Permitir o intermediário 2 acrescenta repetições de , dando uma expressão equivalente a .
A palavra aaabcc pertence à linguagem; aaacc não, porque falta a transição que chega ao estado final. e 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á etapas e pares, logo 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 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.
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.