• Nenhum resultado encontrado

DFA para DFA mínimo: Algoritmo de Hopcroft

Computações de ponto fixo

2.4.4 DFA para DFA mínimo: Algoritmo de Hopcroft

Como melhoria final na conversão RE→DFA, podemos acrescentar um algoritmo para minimizar o número de estados no DFA. O DFA que surge da construção de subconjunto pode ter um grande conjunto de estados. Embora isso não aumente o tempo necessário para varrer uma string, aumenta o tamanho do reconhecedor na memória. Em computadores modernos, a velocidade de acessos à memória constantemente O uso de um conjunto de vetores de bits para a worklist

pode garantir que o algoritmo não possui cópias duplicadas do nome de um nó na worklist. Ver Apêndice B.2.

governa a velocidade da computação. Um reconhecedor pequeno pode caber melhor na memória cache do processador.

Para minimizar o número de estados em um DFA, (D, O, d, d0, DA), precisamos de uma técnica para detectar quando dois estados são equivalentes — ou seja, quando ambos produzem o mesmo comportamento sobre qualquer string de entrada. O algoritmo na Figura 2.9 encontra classes de equivalência de estados do DFA com base no seu comportamento. A partir dessas classes de equivalência podemos construir um DFA mínimo.

O algoritmo constrói uma partição de conjunto, P = {p1, p2, p3,. .. pm}, dos estados do DFA. A partição em particular, P, que o algoritmo constrói agrupa os estados do DFA por seu comportamento. Dois estados do DFA, di, dj ∈ ps, têm o mesmo comportamento em resposta a todos os caracteres de entrada, ou seja, se di →c dx, dj →cdy e di, dj ∈ ps; então, dx e dy precisam estar no mesmo conjunto pt. Essa propriedade é mantida para cada conjunto ps ∈ P, para cada par de estados di, dj ∈ ps e para cada caractere de entrada, c. Assim, os estados em ps têm o mesmo comportamento com relação aos caracteres de entrada e aos conjuntos restantes em P.

Para minimizar um DFA, cada conjunto ps ∈ P deve ser o maior possível dentro da restrição de equivalência comportamental. Para construir essa partição, o algoritmo começa com uma inicial aproximada, que obedece a todas as propriedades, exceto a equivalência comportamental. Depois, refina iterativamente essa partição para impor a equivalência comportamental. A partição inicial contém dois conjuntos, p0 = DA e

p1 = {D – DA}, separação que garante que nenhum conjunto na partição final contenha estados de aceitação e de não aceitação, pois o algoritmo nunca combina duas partições. O algoritmo refina a partição inicial examinando repetidamente cada ps ∈ P para procurar estados em ps que tenham comportamento diferente para alguma string de entrada. Logicamente, ele não pode rastrear o comportamento do DFA em cada string. Porém, pode simular o comportamento de determinado estado em resposta a um único caractere de entrada. E usa uma condição simples para refinar a partição: um símbolo c ∈ O deverá produzir o mesmo comportamento para cada estado di ∈ ps. Se não, o algoritmo divide ps devido a c.

Essa ação de divisão é a chave para entender o algoritmo. Para di e dj permanecerem juntos em ps, ambos devem tomar transições equivalentes em cada caractere c ∈ O. Ou seja, ∀c ∈ O, di →c dx e dj →cdy, onde dx, dy ∈ pt. Qualquer estado dk ∈ ps, onde dk →c dz, dz ∉ pt, não pode permanecer na mesma partição de di e dj. De modo

di⟶cdx dj⟶cdy Partição de conjunto

Partição de conjunto de S é uma coleção de subconjuntos disjuntos, não vazios, de S, cuja união é exatamente S.

di⟶cdx dj⟶cdy

dk⟶cdz

semelhante, se di e dj tiverem transições em c, e dk não, este não pode permanecer na mesma partição que di e dj.

A Figura 2.10 torna isso concreto. Os estados em p1 = {di, dj, dk} são equivalentes se e somente se suas transições, ∀c ∈ O, os levarem a estados que, por si sós, estão em uma classe de equivalência. Como vemos, cada estado tem uma transição em a:

 →a

di dx,  → a

dj dy e dk →adz. Se dx, dy e dz estiverem todos no mesmo conjunto na partição atual, como pode ser visto à esquerda, então di, dj e dk deverão permanecer juntos, e a não divide p1.

Por outro lado, se dx, dy e dz estiverem em dois ou mais conjuntos diferentes, então a divide p1. Como vemos no desenho do centro da Figura 2.10, dx ∈ p2 enquanto dy e

dz ∈ p3, de modo que o algoritmo precisa dividir p1 e construir dois novos conjuntos,

P4 = {di} e p5 = {dj , dk}, para refletir o potencial para diferentes resultados com strings que começam com o símbolo a. O resultado aparece no lado direito da mesma figura. A mesma divisão resultaria se o estado di não tivesse transição em a.

Para refinar uma partição P, o algoritmo examina cada p ∈ P e cada c ∈ O. Se c divide p, o algoritmo constrói dois novos conjuntos a partir de p e os acrescenta a T. (Ele poderia dividir p em mais de dois conjuntos, todos tendo comportamentos internamente consistentes em c. No entanto, criar um estado consistente e agrupar o restante de p em outro estado será suficiente. Se o último estado for inconsistente em seu comportamento sobre c, o algoritmo o dividirá em uma iteração posterior.) O algoritmo repete esse processo até que encontre uma partição em que não possa mais dividir conjuntos. Para construir o novo DFA a partir da partição final p, podemos criar um único estado para representar cada conjunto p ∈ P e acrescentar transições apropriadas entre esses novos estados representativos. Para o estado representando pl, acrescentamos uma transição para o estado representando pm sobre c se algum dj ∈ pl tiver uma transição em c para algum dk ∈ pm. Pela construção, sabemos que, se dj tiver tal transição, o mesmo ocorre com cada outro em pl; não fosse assim, o algoritmo teria dividido pl devido a c. O DFA resultante é mínimo; a prova está fora do nosso escopo.

Exemplos

Considere um DFA que reconhece a linguagem fee | fie, mostrada na Figura 2.11a. Por inspeção, podemos ver que os estados s3 e s5 servem à mesma finalidade. Ambos estão

aceitando estados entrados apenas por uma transição na letra e. Nenhum tem transição didj⟶adx⟶ady

dk⟶adz

que sai do estado. Espera-se que o algoritmo de minimização de DFA descubra este fato e os substitua por um único estado.

A Figura 2.11b mostra as etapas significativas que ocorrem na minimização deste DFA. A partição inicial, mostrada como a etapa 0, separa os estados de aceitação dos de não aceitação. Supondo que o laço while no algoritmo percorra os conjuntos de P em ordem, e sobre os caracteres em O = {e, f, i} em ordem, então, primeiro examina o conjunto {s3, s5}. Como nenhum estado tem transição de saída, não é dividido em

qualquer caractere. Na segunda etapa, ele examina {s0, s1, s2, s4}; no caractere e, divide

{s2, s4} do conjunto. Na terceira, examina {s0, s1} e o divide devido ao caractere f.

Nesse ponto, a partição é {{s3, s5}, {s0}, {s1}, {s2, s4}}. O algoritmo faz uma passagem

final sobre os conjuntos na partição, não divide nenhum deles, e termina.

Para construir o novo DFA, devemos construir um estado para representar cada con- junto na partição final, acrescentar as transições apropriadas a partir do DFA original e designar estados inicial e de aceitação. A Figura 2.11c mostra o resultado para este exemplo.

Como segundo exemplo, considere o DFA para a(b|c)*, produzido pela construção de Thompson, e pela construção de subconjunto, mostrado na Figura 2.12a. A primeira etapa do algoritmo de minimização constrói uma partição inicial {{d0}, {d1, d2, d3}},

como mostramos ao lado. Como p1 tem apenas um estado, não pode ser dividido.

Quando o algoritmo examina p2, não encontra transições sobre a a partir de qualquer

estado em p2. Para b e c, cada estado tem uma transição de volta para p2. Assim, ne-

nhum símbolo em O divide p2, e a partição final é {{d0}, {d1, d2, d3}}.

O DFA mínimo resultante aparece na Figura 2.12b. Lembre-se de que este é o DFA que sugerimos que um humano derivaria. Após a minimização, as técnicas automáticas produzem o mesmo resultado.

Este algoritmo é outro exemplo de computação de ponto fixo. P é finito; no máximo, pode conter |D| elementos. O laço while divide conjuntos em P, mas nunca os combina. Assim, |P| cresce monotonicamente. O laço termina quando alguma iteração não divide conjuntos em P. O comportamento de pior caso ocorre quando cada estado no DFA comporta-se de modo diferente; neste caso, o laço while termina quando P tem um conjunto distinto para cada di ∈ D. Isto ocorre quando o algoritmo é aplicado a um DFA mínimo.