Equivalência de NFAs e DFAs
REPRESENTANDO A PRECEDÊNCIA DE OPERADORES
A construção de Thompson deve aplicar suas três transformações em uma ordem que seja consistente com a precedência dos operadores na expressão regular. Para representar esta ordem, uma implementação desta construção pode montar uma árvore que representa a expressão regular e sua precedência interna. A RE a(b|c)* produz a seguinte árvore:
onde + representa concatenação, | alternação e * fechamento. Os parênteses são transformados na estrutura da árvore, e, assim, não possuem representação explícita. A construção aplica as transformações individuais em um percurso de pós-ordem pela árvore. Como as transformações correspondem às operações, este percurso monta a seguinte sequência de NFAs: a, b, c, b|c, (b|c)* e, finalmente, a(b|c)*. Os Capítulos 3 e 4 mostram como montar árvores de expressão.
2.4.3 NFA para DFA: A construção de subconjunto
A construção de Thompson produz um NFA para reconhecer a linguagem especi- ficada por uma RE. Como a execução do DFA é muito mais fácil de simular do que a do NFA, o próximo passo no ciclo de construções converte o NFA montado pela construção de Thompson em um DFA que reconhece a mesma linguagem. Os DFAs resultantes possuem um modelo de execução simples e várias implementações eficientes. O algoritmo que constrói um DFA a partir de um NFA é chamado cons- trução de subconjunto.
A construção de subconjunto usa como entrada um NFA (N, O, dN, n0, NA). Ele produz um DFA, (D, O, dD, d0, DA). NFA e DFA utilizam o mesmo alfabeto, O. O estado inicial do DFA, d0, e seus estados de aceitação, DA, surgirão a partir da construção. A parte complexa da construção é a derivação do conjunto de estados D do DFA a partir do conjunto de estados N do NFA, e a derivação da função de transição do DFA, dD.
O algoritmo, mostrado na Figura 2.6, constrói um conjunto Q cujos elementos, qi, são, cada um, um subconjunto de N, ou seja, cada qi ∈ 2N. Quando o algoritmo termina, cada
qi ∈ Q corresponde a um estado, di ∈ D, no DFA. A construção monta os elementos de Q seguindo as transições que o NFA pode fazer sobre determinada entrada. Assim, cada qi representa uma configuração válida do NFA.
O algoritmo começa com um conjunto inicial, q0, que contém n0 e quaisquer estados
no NFA que possam ser alcançados a partir de n0 ao longo dos caminhos que contêm
Configuração válida
Configuração de um NFA que pode ser alcançada por alguma string de entrada.
apenas ∈-transições. Esses estados são equivalentes, pois podem ser alcançados sem consumir entrada.
Para construir q0 a partir de n0, o algoritmo calcula ∈-closure(n0), e usa, como en-
trada, um conjunto S de estados do NFA. Ele retorna um conjunto de estados do NFA construídos a partir de S da seguinte forma: ∈-closure examina cada estado si ∈ S e acrescenta a S qualquer estado alcançável seguindo uma ou mais ε-transições a partir de si. Se S for o conjunto de estados que podem ser alcançados a partir de n0 seguindo os
caminhos rotulados com abc, então ∈-closure(S) é o conjunto de estados que podem ser alcançados a partir de n0 seguindo os caminhos rotulados abc∈*. Inicialmente, Q
tem apenas um membro, q0, e a WorkList contém q0.
O algoritmo prossegue removendo um conjunto q da worklist (lista de trabalho). Cada q representa uma configuração válida do NFA original. O algoritmo constrói, para cada caractere c no alfabeto O, a configuração que o NFA alcançaria se lesse c enquanto estiver na configuração q. Essa computação usa uma função Delta(q,c), que aplica a função de transição do NFA a cada elemento de q, e retorna Us∈qidN(s,c).
O laço while remove repetidamente uma configuração q da worklist e usa Delta para calcular suas transições em potencial. Ele aumenta essa configuração calculada com quaisquer estados que possam ser alcançados pelas ε-transições seguintes, e acrescenta quaisquer novas configurações geradas dessa forma a Q e à worklist. Quando descobre uma nova configuração t alcançável a partir de q sobre o caractere c, o algoritmo registra essa transição na tabela T. O laço interno, que percorre o alfabeto para cada configuração, realiza uma busca completa.
Observe que Q cresce monotonicamente. O laço while acrescenta conjuntos a Q, mas nunca os remove. Como o número de configurações do NFA é limitado e cada confi- guração só aparece uma vez na worklist, o laço while deve parar. Ao parar, Q contém todas as configurações válidas do NFA e T mantém todas as transições entre elas. Q pode se tornar grande — pode chegar a |2N| estados distintos. A quantidade de não determinismo encontrada no NFA determina quanta expansão de estado ocorre. Lembre-se,
■
porém, que o resultado é um DFA que faz exatamente uma transição por caractere de en- trada, independente do número de estados no DFA. Assim, qualquer expansão introduzida pela construção de subconjunto não afeta o tempo de execução do DFA.
De Q a D
Quando a construção de subconjunto para, terá construído um modelo do DFA desejado, que simula o NFA original. A construção do DFA a partir de Q e T é sim- ples. Cada qi ∈ Q precisa de um estado di ∈ D para representá-lo. Se qi contém um estado de aceitação do NFA, então di é um estado de aceitação do DFA. Podemos construir a função de transição, dD, diretamente de T, observando o mapeamento de qi para di. Finalmente, o estado construído a partir de q0 torna-se d0, o estado
inicial do DFA.
Exemplo
Considere o NFA construído para a(b|c)* na Seção 2.4.2 e mostrado na Figu- ra 2.7a com seus estados renumerados. A tabela na Figura 2.7b esboça as etapas seguidas pelo algoritmo de construção de subconjunto. A primeira coluna mostra o nome do conjunto em Q sendo processado em determinada iteração do laço while. A segunda, o nome do estado correspondente no novo DFA. A terceira, o conjunto de estados do NFA contidos no conjunto atual de Q. As três colunas finais exibem resultados do cálculo de ∈-closure de Delta no estado para cada caractere em O.
O algoritmo realiza as seguintes etapas:
1. A inicialização define q0 como ∈-closure({n0}), que é simplesmente n0. A
primeira iteração calcula ∈-closure(Delta(q0, a)), que contém seis estados do
NFA, e ∈-closure(Delta(q0,b)) e ∈-closure(Delta(q0,c)), que são vazios.
2. A segunda iteração do laço while examina q1; produz duas configurações e as
chama de q2 e q3.
3. A terceira iteração do laço while examina q2 e constrói duas configurações, que
são idênticas a q2 e q3.
4. A quarta iteração do laço while examina q3. Assim como a terceira iteração, esta
reconstrói q2 e q3.
A Figura 2.7c mostra o DFA resultante; os estados correspondem aos do DFA a partir da tabela, e as transições são dadas pelas operações Delta que geram esses estados. Como os conjuntos q1, q2 e q3 contêm n9 em cada um deles (o estado de aceitação do