Минимизация ДКА, алгоритм Хопкрофта (сложность O(n log n)) — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(reformatting a bit)
(Простой алгоритм)
 
(не показано 17 промежуточных версий 5 участников)
Строка 18: Строка 18:
 
# Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>\langle F,\ c \rangle</tex> и <tex>\langle Q \setminus F, c \rangle</tex> помещаются в очередь.
 
# Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>\langle F,\ c \rangle</tex> и <tex>\langle Q \setminus F, c \rangle</tex> помещаются в очередь.
 
# Из очереди извлекается пара <tex>\langle C,\ a \rangle</tex>, <tex>C</tex> далее именуется как сплиттер.
 
# Из очереди извлекается пара <tex>\langle C,\ a \rangle</tex>, <tex>C</tex> далее именуется как сплиттер.
# Каждый класс <tex>R</tex> текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>a</tex> переходят в сплиттер (<tex>R_1</tex>), а второй из всех оставшихся (<tex>R_2</tex>).  
+
# Каждый класс <tex>R</tex> текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>a</tex> переходят в сплиттер <tex>(R_1)</tex>, а второй из всех оставшихся <tex>(R_2)</tex>.  
# Если <tex>R</tex> разбился на два непустых подкласса (т.е. <tex> R_1 \ne \emptyset \ \land \  R_2 \ne \emptyset </tex>).
+
# Если <tex>R</tex> разбился на два непустых подкласса (то есть <tex> R_1 \ne \emptyset \ \land \  R_2 \ne \emptyset </tex>).
 
## В разбиении <tex>P</tex> класс <tex>R</tex> заменяется на свои подклассы <tex>R_1</tex> и <tex>R_2</tex>.
 
## В разбиении <tex>P</tex> класс <tex>R</tex> заменяется на свои подклассы <tex>R_1</tex> и <tex>R_2</tex>.
 
## Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>\langle R_1, c \rangle</tex> и <tex>\langle R_2, c \rangle</tex> помещаются в очередь.
 
## Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>\langle R_1, c \rangle</tex> и <tex>\langle R_2, c \rangle</tex> помещаются в очередь.
Строка 32: Строка 32:
 
*<tex>\mathtt{R}</tex> {{---}} класс состояний ДКА.
 
*<tex>\mathtt{R}</tex> {{---}} класс состояний ДКА.
  
   <tex>\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)</tex>:
+
   '''function''' findEquivalenceClasses<tex>(Q,\ F,\ \delta)</tex>: '''vector'''
 
     <tex>\mathtt{P} \leftarrow \{ F,\ Q \setminus F \}</tex>
 
     <tex>\mathtt{P} \leftarrow \{ F,\ Q \setminus F \}</tex>
 
     <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
     <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
     '''for''' <tex>c \in \Sigma</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
       '''push''' <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{S}</tex>
+
       push <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{S}</tex>
 
     '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
 
     '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex>
+
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> pop '''from''' <tex>\mathtt{S}</tex>
 
       '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex>  
 
       '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex>  
 
         <tex> R_1, R_2 \leftarrow </tex> <tex>\mathtt{split}(R,\ C,\ a)</tex>
 
         <tex> R_1, R_2 \leftarrow </tex> <tex>\mathtt{split}(R,\ C,\ a)</tex>
 
         '''if''' <tex> R_1 \ne \varnothing </tex> '''and''' <tex> R_2 \ne \varnothing </tex>
 
         '''if''' <tex> R_1 \ne \varnothing </tex> '''and''' <tex> R_2 \ne \varnothing </tex>
        '''replace''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> '''with''' <tex>R_1</tex> '''and''' <tex>R_2</tex>
+
          replace <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> with <tex>R_1</tex> '''and''' <tex>R_2</tex>
        '''for''' <tex>c \in \Sigma</tex>
+
          '''for''' <tex>c \in \Sigma</tex>
          '''insert''' <tex>\langle R_1,\ c \rangle</tex> '''in''' <tex>\mathtt{S}</tex>
+
            insert <tex>\langle R_1,\ c \rangle</tex> '''in''' <tex>\mathtt{S}</tex>
          '''insert''' <tex>\langle R_2,\ c \rangle</tex> '''in''' <tex>\mathtt{S}</tex>
+
            insert <tex>\langle R_2,\ c \rangle</tex> '''in''' <tex>\mathtt{S}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
  
Строка 86: Строка 86:
  
 
=== Реализация ===
 
=== Реализация ===
<tex>\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2)</tex> {{---}} функция, которая добавляет пары <tex>\langle R_1, \forall c \in \Sigma \rangle</tex>, <tex>\langle R_2, \forall c \in \Sigma \rangle</tex> в очередь S.
 
  
 
*<tex>\mathtt{Q}</tex> {{---}} множество состояний ДКА,
 
*<tex>\mathtt{Q}</tex> {{---}} множество состояний ДКА,
Строка 95: Строка 94:
 
*<tex>\mathtt{R}</tex> {{---}} класс состояний ДКА.
 
*<tex>\mathtt{R}</tex> {{---}} класс состояний ДКА.
  
   <tex>\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)</tex>:
+
   '''function''' findEquivalenceClasses<tex>(Q,\ F,\ \delta)</tex>: '''vector'''
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
     <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
     '''for''' <tex>c \in \Sigma</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
       '''push''' <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{S}</tex>
+
       push <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{S}</tex>
 
     '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
 
     '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex>
+
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> pop '''from''' <tex>\mathtt{S}</tex>
 
       '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex>  
 
       '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex>  
 
         <tex> R_1, R_2 \leftarrow </tex> <tex>\mathtt{split}(R,\ C,\ a)</tex>
 
         <tex> R_1, R_2 \leftarrow </tex> <tex>\mathtt{split}(R,\ C,\ a)</tex>
         '''if''' <tex> R_1 \ne \varnothing </tex> '''and''' <tex> R_2 \ne \varnothing </tex>
+
         '''if''' <tex> R_1 \ne \varnothing </tex> '''and''' <tex> R_2 \ne \varnothing </tex>
         '''replace''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> '''with''' <tex>R_1</tex> '''and''' <tex>R_2</tex>
+
        replace <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> with' <tex>R_1</tex> '''and''' <tex>R_2</tex>
         <tex>\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2)</tex>
+
         '''if''' <tex>\langle R,\ c \rangle</tex> '''in''' <tex> \mathtt{S}</tex> <font color=darkgreen>// смотрим, есть ли пара <tex>\langle R,\ c \rangle</tex> в очереди </font>
 +
          remove <tex>\langle R, c \rangle</tex> '''from''' <tex>\mathtt{S}</tex> <font color=darkgreen>// заменяем её на пары <tex>\langle R_1, c \rangle</tex>, <tex>\langle R_2, c \rangle</tex> если пара есть </font>
 +
          push <tex>\langle R_1, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 +
          push <tex>\langle R_2, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 +
         '''else'''
 +
            '''if''' <tex> |\mathtt{P}[R_1]| \leqslant |\mathtt{P}[R_2]| </tex> <font color=darkgreen>// вставляем любую иначе</font>
 +
              push <tex>\langle R_1, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 +
            '''else'''
 +
              push <tex>\langle R_2, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 +
 +
 
 +
       
  
 
Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за <tex>T'</tex> (его нужно будет эффективно находить для каждой пары <tex>\langle C,\ a \rangle</tex>).
 
Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за <tex>T'</tex> (его нужно будет эффективно находить для каждой пары <tex>\langle C,\ a \rangle</tex>).
  
   <tex>\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)</tex>:
+
   '''function''' findEquivalenceClasses<tex>(Q,\ F,\ \delta)</tex>: '''vector'''
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
     <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
     '''for''' <tex>c \in \Sigma</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
       '''push''' <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{S}</tex>
+
       push <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{S}</tex>
 
     '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
 
     '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex>
+
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> pop '''from''' <tex>\mathtt{S}</tex>
 
       <tex>\mathtt{Inverse} \leftarrow \{r \ | \ r \in Q, \ \delta(r, a) \in C\}</tex>
 
       <tex>\mathtt{Inverse} \leftarrow \{r \ | \ r \in Q, \ \delta(r, a) \in C\}</tex>
       <tex>T' \leftarrow \{R \ | \ R \in \mathtt{P}, \ R \cap \mathtt{Inverse} \neq \varnothing\}</tex>
+
       <tex>T' \leftarrow \{R \ | \ R \in \mathtt{P}, \ R \cap \mathtt{Inverse} \neq \varnothing\}</tex> <font color=darkgreen>// находим классы, из состояний которых есть ребро в состояния сплиттера </font>
       '''for''' <tex>R</tex> '''in''' <tex>T'</tex>  
+
       '''for''' <tex>R</tex> '''in''' <tex>T'</tex> <font color=darkgreen>// перебираем только классы входящие в <tex>T'</tex></font>
 
         <tex> R_1, R_2 \leftarrow </tex> <tex>\mathtt{split}(R,\ C,\ a)</tex>
 
         <tex> R_1, R_2 \leftarrow </tex> <tex>\mathtt{split}(R,\ C,\ a)</tex>
 
         '''if''' <tex> R_1 \ne \varnothing </tex> '''and''' <tex> R_2 \ne \varnothing </tex>
 
         '''if''' <tex> R_1 \ne \varnothing </tex> '''and''' <tex> R_2 \ne \varnothing </tex>
         '''replace''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> '''with''' <tex>R_1</tex> '''and''' <tex>R_2</tex>
+
        replace <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> with <tex>R_1</tex> '''and''' <tex>R_2</tex>
         <tex>\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2)</tex>
+
         '''if''' <tex>\langle R,\ c \rangle</tex> '''in''' <tex> \mathtt{S}</tex>
 +
          remove <tex>\langle R, c \rangle</tex> '''from''' <tex>\mathtt{S}</tex>
 +
          push <tex>\langle R_1, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 +
          push <tex>\langle R_2, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 +
         '''else'''
 +
            '''if''' <tex> |\mathtt{P}[R_1]| \leqslant |\mathtt{P}[R_2]| </tex>
 +
              push <tex>\langle R_1, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 +
            '''else'''
 +
              push <tex>\langle R_2, c \rangle</tex> '''into''' <tex>\mathtt{S}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
  
  
Каждая итерация цикла <tex> \mathrm{while} </tex> может быть выполнена за <tex> O(|Q| + |\mathtt{Inverse}|) </tex> для текущей пары <tex>\langle C,\ a \rangle</tex>. Покажем, как можно достичь этой оценки.
+
Каждая итерация цикла <tex> \mathrm{while} </tex> может быть выполнена за <tex> O(|Q| + |\mathtt{Inverse}|)\,</tex> для текущей пары <tex>\langle C,\ a \rangle</tex>. Покажем, как можно достичь этой оценки.
  
 
Классы разбиения <tex>P</tex> будем поддерживать с помощью множеств на [[Хеш-таблица | хэш-таблицах]] (само же разбиение {{---}} обычный вектор, индекс {{---}} номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за <tex>O(1)</tex>).
 
Классы разбиения <tex>P</tex> будем поддерживать с помощью множеств на [[Хеш-таблица | хэш-таблицах]] (само же разбиение {{---}} обычный вектор, индекс {{---}} номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за <tex>O(1)</tex>).
Строка 134: Строка 152:
 
*<tex>\mathtt{Class}[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex>,
 
*<tex>\mathtt{Class}[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex>,
 
*<tex>\mathtt{Queue}</tex> {{---}} очередь пар <tex>\langle C,\ a \rangle</tex>, где <tex>C</tex> {{---}} номер класса (сплиттера),
 
*<tex>\mathtt{Queue}</tex> {{---}} очередь пар <tex>\langle C,\ a \rangle</tex>, где <tex>C</tex> {{---}} номер класса (сплиттера),
*<tex>\mathtt{Inv}[r][a]</tex> {{---}} массив состояний, из которых есть ребра по символу <tex>a</tex> в состояние <tex>r</tex> (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма),
+
*<tex>\mathtt{Inv}[r][a]</tex> {{---}} массив состояний, из которых есть ребра по символу <tex>a</tex> в состояние <tex>r</tex> (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма).
  
Для обработки <tex>T'</tex> за <tex>O(|Q| + |\mathtt{Inverse}|)</tex> нам понадобится следующая структура:
+
Для обработки <tex>T'</tex> за <tex>O(|Q| + |\mathtt{Inverse}|)\,</tex> нам понадобится следующая структура:
 
*<tex>\mathtt{Involved}</tex> {{---}} список из номеров классов, содержащихся во множестве <tex>T'</tex>,
 
*<tex>\mathtt{Involved}</tex> {{---}} список из номеров классов, содержащихся во множестве <tex>T'</tex>,
 
*<tex>\mathtt{Count}</tex> {{---}} целочисленный массив, где <tex>\mathtt{Count}[i]</tex> хранит количество состояний из класса <tex>i</tex>, которые содержатся в <tex>\mathtt{Inverse}</tex>,
 
*<tex>\mathtt{Count}</tex> {{---}} целочисленный массив, где <tex>\mathtt{Count}[i]</tex> хранит количество состояний из класса <tex>i</tex>, которые содержатся в <tex>\mathtt{Inverse}</tex>,
 
*<tex>\mathtt{Twin}</tex> {{---}} массив, хранящий в <tex>\mathtt{Twin}[i]</tex> номер нового класса, образовавшегося при разбиении класса <tex>i</tex>.
 
*<tex>\mathtt{Twin}</tex> {{---}} массив, хранящий в <tex>\mathtt{Twin}[i]</tex> номер нового класса, образовавшегося при разбиении класса <tex>i</tex>.
  
   <tex>\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)</tex>:
+
   '''function''' findEquivalenceClasses<tex>(Q,\ F,\ \delta)</tex>: '''vector'''
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
       '''push''' <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{Queue}</tex>
+
       push <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{Queue}</tex>
 
     '''while''' <tex>\mathtt{Queue} \ne \varnothing</tex>
 
     '''while''' <tex>\mathtt{Queue} \ne \varnothing</tex>
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{Queue}</tex>
+
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> pop '''from''' <tex>\mathtt{Queue}</tex>
 
       <tex>\mathtt{Involved} \leftarrow \varnothing</tex>
 
       <tex>\mathtt{Involved} \leftarrow \varnothing</tex>
 
       '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
       '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
         <tex>i = \mathtt{Class}[r]</tex>
 
         <tex>i = \mathtt{Class}[r]</tex>
 
         '''if''' <tex>\mathtt{Count}[i] == 0</tex>
 
         '''if''' <tex>\mathtt{Count}[i] == 0</tex>
           '''insert''' <tex>i</tex> '''in''' <tex>\mathtt{Involved}</tex>
+
           insert <tex>i</tex> '''into''' <tex>\mathtt{Involved}</tex>
 
         <tex>\mathtt{Count}[i]++</tex>
 
         <tex>\mathtt{Count}[i]++</tex>
 
       '''for''' <tex> i \in \mathtt{Involved}</tex>
 
       '''for''' <tex> i \in \mathtt{Involved}</tex>
         '''if''' <tex>\mathtt{Count}[i] <</tex> '''size of''' <tex>\mathtt{P}[i]</tex>
+
         '''if''' <tex>\mathtt{Count}[i] < |\mathtt{P}[i]|</tex>
             '''insert''' <tex>\{\}</tex> '''into''' <tex>\mathtt{P}</tex> <font color=darkgreen>//Создадим пустой класс в разбиении <tex>\mathtt{P}</tex></font>
+
             insert <tex>\{\}</tex> '''into''' <tex>\mathtt{P}</tex> <font color=darkgreen>// создадим пустой класс в разбиении <tex>\mathtt{P}</tex></font>
             <tex>\mathtt{Twin[i]} = </tex> '''size of''' <tex>\mathtt{P}</tex> <font color=darkgreen>//Запишем в <tex>\mathtt{Twin[i]}</tex> индекс нового класса</font>
+
             <tex>\mathtt{Twin}[i] = |\mathtt{P}|</tex> <font color=darkgreen> //запишем в <tex>\mathtt{Twin[i]}</tex> индекс нового класса</font>
 
       '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
       '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
         <tex>i = \mathtt{Class}[r]</tex>
 
         <tex>i = \mathtt{Class}[r]</tex>
         '''if''' <tex>\mathtt{Twin}[i] \neq 0</tex>
+
         <tex>j = \mathtt{Twin}[i]</tex>
            '''remove''' <tex>r</tex> '''from''' <tex>\mathtt{P}[i]</tex>
+
        '''if''' <tex>j \neq 0</tex>
             '''add''' <tex>r</tex> '''to''' <tex>\mathtt{P}[\mathtt{Twin}[i]]</tex>
+
            remove <tex>r</tex> '''from''' <tex>\mathtt{P}[i]</tex>
            <tex>\mathtt{Class}[r] = \mathtt{Twin}[i]</tex>
+
             add <tex>r</tex> '''to''' <tex>\mathtt{P}[j]</tex>
      '''for''' <tex> j \in \mathtt{Involved}</tex>
+
      '''for''' <tex> i \in \mathtt{Involved}</tex>
        '''if''' <tex> \mathtt{Twin}[j] \neq 0 </tex>
+
        <tex>j = \mathtt{Twin}[i]</tex>
             <tex>\mathtt{pushSetsToQueue}(\mathtt{Queue},\ j,\ \mathtt{Twin}[j])</tex>
+
        '''if''' <tex> j \neq 0 </tex>
         <tex>\mathtt{Count}[j] = 0</tex>
+
          '''if''' <tex>|\mathtt{P}[j]| > |\mathtt{P}[i]|</tex>  <font color=darkgreen>// парный класс должен быть меньшего размера</font>
         <tex>\mathtt{Twin}[j] = 0</tex>
+
             <tex>\mathtt{swap}(\mathtt{P}[i],\ \mathtt{P}[j])</tex> <font color=darkgreen>// swap за <tex>\mathtt{O(1)}</tex> {{---}} просто переставить указатели</font>
 +
          '''for''' <tex>r \in \mathtt{P}[j]</tex> <font color=darkgreen> // обновляем номера классов для вершин, у которых они изменились</font>
 +
            <tex>\mathtt{Class}[r] = j</tex>
 +
          '''for''' <tex>c \in \Sigma</tex>
 +
            push <tex>\langle j, c \rangle</tex> '''to''' <tex>\mathtt{Queue}</tex>
 +
         <tex>\mathtt{Count}[i] = 0</tex>
 +
         <tex>\mathtt{Twin}[i] = 0</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
  
Стоит отметить, что массивы <tex>\mathtt{Count}, \mathtt{Twin}</tex> аллоцируются ровно один раз при инициализации алгоритма.
 
 
Осталось только реализовать <tex>\mathtt{pushSetsToQueue}</tex>.
 
 
  <tex>\mathtt{pushSetsToQueue}(\mathtt{Queue},\ R_1,\ R_2)</tex>:
 
      <tex>\mathrm{cnt1}  \leftarrow </tex> '''size of''' <tex>\mathtt{P}[R_1]</tex>
 
      <tex>\mathrm{cnt2}  \leftarrow </tex> '''size of''' <tex>\mathtt{P}[R_2]</tex>
 
      '''if''' <tex> \mathrm{cnt1} \leqslant \mathrm{cnt2} </tex>
 
        <tex>\mathtt{swapClasses}(R_1,\ R_2)</tex>
 
      '''for''' <tex>c \in \Sigma</tex>
 
        '''push''' <tex>\langle R_2, c \rangle</tex> '''to''' <tex>\mathtt{Queue}</tex>
 
  
  <tex>\mathtt{swapClasses}(i,\ j)</tex>:
+
Стоит отметить, что массивы <tex>\mathtt{Count},\ \mathtt{Twin}\,</tex> аллоцируются ровно один раз при инициализации алгоритма.
      <tex>\mathtt{swap}(\mathtt{P}[i],\ \mathtt{P}[j])</tex> <font color=darkgreen>//Поменять друг с другом содержимое индексов <tex>i,\ j</tex> в <tex>\mathtt{P}</tex></font>
 
      '''for''' <tex>r</tex> '''in''' <tex>\mathtt{P}[i]</tex>
 
        <tex>\mathtt{Class}[r] = j</tex>
 
      '''for''' <tex>r</tex> '''in''' <tex>\mathtt{P}[j]</tex>
 
        <tex>\mathtt{Class}[r] = i</tex>
 
  
Стоит пояснить, зачем требуется менять содержимое множеств (<tex>\mathtt{swapClasses}(R_1,\ R_2)</tex>). Почему нельзя просто проверить <tex> \mathrm{cnt1} \leqslant \mathrm{cnt2} </tex>, и на основе этого добавить либо первое, либо второе? Ответ кроется в том, что де-факто мы создаем только один новый класс, старый класс (<tex>R_1</tex>) мы только меняем в размерах. Потому если <tex>\langle R_1, c \rangle</tex> уже есть в очереди, у нас не остается иного выбора, кроме как добавить <tex>\langle R_2, c \rangle</tex>. Однако, может случится, что <tex>|R_2| > |R_1| </tex>, что как раз и может потенциально дать квадратичную ассимптотику (логарифмическая достигается как раз за счет того, что добавляемый в очередь подкласс - меньший).
+
Также стоит отметить, что собственно наличие/отсутствие пары в очереди можно не проверять. Если для некоторого <tex>c</tex> пара <tex>\langle i, c \rangle</tex> уже была в очереди, то мы добавим её "вторую половинку" <tex>\langle \mathtt{Twin}[i], c \rangle</tex>. Если её в очереди не было, то мы вольны сами выбирать, какой подкласс добавлять в очередь, и таким образом добавляем опять же <tex>\langle \mathtt{Twin}[i], c \rangle</tex>.
 
+
Кроме того, вместо очереди можно использовать вообще произвольную структуру, хранящую элементы, в том числе стэк, множество, так как порядок извлечения нам по сути не важен.
Причем, стоит отметить, что собственно наличие/отсутствие пары в очереди можно не проверять. Если для некоторого <tex>c</tex> пара <tex>\langle R_1, c \rangle</tex> уже была в очереди, то мы добавим её "вторую половинку" <tex>\langle R_2, c \rangle</tex>. Если её в очереди не было, то мы вольны сами выбирать, какой подкласс добавлять в очередь, и таким образом добавляем опять же <tex>\langle R_2, c \rangle</tex>.
 
  
 
===Время работы===
 
===Время работы===
Строка 230: Строка 237:
 
<tex>\sum |\mathtt{Inverse}|</tex> по всем итерациям цикла <tex>\mathrm{while}</tex> не превосходит <tex>|\Sigma| |Q| \log_2(|Q|)</tex>.
 
<tex>\sum |\mathtt{Inverse}|</tex> по всем итерациям цикла <tex>\mathrm{while}</tex> не превосходит <tex>|\Sigma| |Q| \log_2(|Q|)</tex>.
 
|proof =
 
|proof =
Пусть <tex>x, y \in Q</tex>, <tex>a \in \Sigma</tex> и <tex> \delta(x, a) = y</tex>. Зафиксируем эту тройку. Заметим, что количество раз, которое <tex>x</tex> встречается в <tex>\mathtt{Inverse}</tex> при условии, что <tex> \delta(x, a) = y</tex>, совпадает с числом удаленных из очереди пар <tex>\langle C,\ a \rangle</tex>, где <tex>y \in C</tex>. Но по [[#Лемма3 | лемме(3)]] эта величина не превосходит <tex>\log_2(|Q|)</tex>. Просуммировав по всем <tex> x \in Q </tex> и по всем <tex> a \in \Sigma</tex> мы получим утверждение леммы.
+
Пусть <tex>x, y \in Q</tex>, <tex>a \in \Sigma</tex> и <tex> \delta(x, a) = y</tex>. Зафиксируем эту тройку. Заметим, что количество раз, которое <tex>x</tex> встречается в <tex>\mathtt{Inverse}\,</tex> при условии, что <tex> \delta(x, a) = y</tex>, совпадает с числом удаленных из очереди пар <tex>\langle C,\ a \rangle</tex>, где <tex>y \in C</tex>. Но по [[#Лемма3 | лемме(3)]] эта величина не превосходит <tex>\log_2(|Q|)</tex>. Просуммировав по всем <tex> x \in Q </tex> и по всем <tex> a \in \Sigma</tex> мы получим утверждение леммы.
 
}}
 
}}
  
Строка 243: Строка 250:
 
*По [[#Лемма2 | второй лемме]] количество итераций цикла <tex>\mathrm{while}</tex> не превосходит <tex>O(|\Sigma| |Q|)</tex>.
 
*По [[#Лемма2 | второй лемме]] количество итераций цикла <tex>\mathrm{while}</tex> не превосходит <tex>O(|\Sigma| |Q|)</tex>.
  
*Операции с множеством <tex>T'</tex> и разбиение классов на подклассы требуют <tex>O(\sum(|\mathtt{Inverse}|))</tex> времени. Но по [[#Лемма4 | лемме(4)]] <tex>\sum(|\mathtt{Inverse}|)</tex> не превосходит <tex>|\Sigma| |Q| \log_2(|Q|)</tex>, то есть данная часть алгоритма выполняется за <tex>O(|\Sigma| |Q| \log_2(|Q|))</tex>.
+
*Операции с множеством <tex>T'</tex> и разбиение классов на подклассы требуют <tex>O(\sum(|\mathtt{Inverse}|))\,</tex> времени. Но по [[#Лемма4 | лемме(4)]] <tex>\sum(|\mathtt{Inverse}|)\,</tex> не превосходит <tex>|\Sigma| |Q| \log_2(|Q|)</tex>, то есть данная часть алгоритма выполняется за <tex>O(|\Sigma| |Q| \log_2(|Q|))</tex>.
  
 
*В [[#Лемма1 | лемме(1)]] мы показали, что в процессе работы алгоритма не может появится больше, чем <tex>2 |Q| - 1</tex> классов, из чего следует, что количество операций <tex>\mathtt{replace}</tex> равно <tex>O(|\Sigma| |Q|)</tex>.
 
*В [[#Лемма1 | лемме(1)]] мы показали, что в процессе работы алгоритма не может появится больше, чем <tex>2 |Q| - 1</tex> классов, из чего следует, что количество операций <tex>\mathtt{replace}</tex> равно <tex>O(|\Sigma| |Q|)</tex>.
Строка 263: Строка 270:
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     <tex>\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
 
     '''for''' <tex>c \in \Sigma</tex>
       '''insert''' <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{Queue}</tex>
+
       insert <tex>\langle F,\ c \rangle</tex>, <tex>\langle Q \setminus F,\ c \rangle</tex> '''into''' <tex> \mathtt{Queue}</tex>
 
     '''while''' <tex>\mathtt{Queue} \ne \varnothing</tex>
 
     '''while''' <tex>\mathtt{Queue} \ne \varnothing</tex>
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> '''take any from''' <tex>\mathtt{Queue}</tex> <font color=darkgreen>//Взять любую пару из <tex>\mathtt{Queue}</tex>, не удаляя (!)</font>
+
       <tex>\langle C,\ a \rangle</tex> <tex>\leftarrow</tex> pop '''from''' <tex>\mathtt{Queue}</tex>
 
       <tex>\mathtt{Involved} = \{\}</tex>
 
       <tex>\mathtt{Involved} = \{\}</tex>
 
       '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
       '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
Строка 271: Строка 278:
 
         '''if''' <tex>\mathtt{Involved}[i] == \varnothing</tex>
 
         '''if''' <tex>\mathtt{Involved}[i] == \varnothing</tex>
 
             <tex>\mathtt{Involved}[i] = \{\}</tex>
 
             <tex>\mathtt{Involved}[i] = \{\}</tex>
         '''add''' <tex>r</tex> '''to''' <tex>\mathtt{Involved}[i]</tex>
+
         add <tex>r</tex> '''to''' <tex>\mathtt{Involved}[i]</tex>
 
       '''for''' <tex> i \in \mathtt{Involved}</tex> <font color=darkgreen>//Перебираем ключи <tex>\mathtt{Involved}</tex></font>
 
       '''for''' <tex> i \in \mathtt{Involved}</tex> <font color=darkgreen>//Перебираем ключи <tex>\mathtt{Involved}</tex></font>
         '''if''' ('''size of''' <tex>\mathtt{Involved}[i] <</tex> '''size of''' <tex>\mathtt{P}[i]</tex>)
+
         '''if''' <tex>|\mathtt{Involved}[i]| < |\mathtt{P}[i]|</tex>
 
             '''insert''' <tex>\{\}</tex> '''into''' <tex>\mathtt{P}</tex> <font color=darkgreen>//Создадим пустой класс в разбиении <tex>\mathtt{P}</tex></font>
 
             '''insert''' <tex>\{\}</tex> '''into''' <tex>\mathtt{P}</tex> <font color=darkgreen>//Создадим пустой класс в разбиении <tex>\mathtt{P}</tex></font>
             <tex>j = </tex> '''size of''' <tex>\mathtt{P}</tex> <font color=darkgreen>//Запишем в <tex>j</tex> индекс нового класса</font>
+
             <tex>j = |\mathtt{P}|</tex> <font color=darkgreen>//Запишем в <tex>j</tex> индекс нового класса</font>
 
             '''for''' <tex>r</tex> '''in''' <tex>\mathtt{Involved}[i]</tex>
 
             '''for''' <tex>r</tex> '''in''' <tex>\mathtt{Involved}[i]</tex>
                '''remove''' <tex>r</tex> '''from''' <tex>\mathtt{P}[i]</tex>
+
              remove <tex>r</tex> '''from''' <tex>\mathtt{P}[i]</tex>
                '''add''' <tex>r</tex> '''to''' <tex>\mathtt{P}[j]</tex>
+
              add <tex>r</tex> '''to''' <tex>\mathtt{P}[j]</tex>
                 <tex>\mathtt{Class}[r] = j</tex>
+
            '''if''' <tex>|\mathtt{P}[j]| > |\mathtt{P}[i]|</tex>  <font color=darkgreen>//Парный класс должен быть меньшего размера</font>
             <tex>\mathtt{pushSetsToQueue}(\mathtt{Queue},\ i,\ j)</tex>
+
                 <tex>\mathtt{swap}(\mathtt{P}[i],\ \mathtt{P}[j])</tex> <font color=darkgreen>//swap за <tex>\mathtt{O(1)}</tex> {{---}} просто переставить указатели</font>
      '''remove''' <tex>\langle C,\ a \rangle</tex> '''from''' <tex> \mathtt{Queue}</tex>
+
             '''for''' <tex>r \in \mathtt{P}[j]</tex> <font color=darkgreen>//Обновляем номера классов для вершин, у которых они изменились</font>
 +
              <tex>\mathtt{Class}[r] = j</tex>
 +
            '''for''' <tex>c \in \Sigma</tex>
 +
              push <tex>\langle j, c \rangle</tex> '''into''' <tex>\mathtt{Queue}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 
     '''return''' <tex>\mathtt{P}</tex>
 
=== Сравнение с алгоритмом из оригинальной статьи Хопкрофта ===
 
 
В оригинальной статье <ref>[http://i.stanford.edu/pub/cstr/reports/cs/tr/71/190/CS-TR-71-190.pdf ''John Hopcroft'' An O(nlogn) algorithm for minimizing states in a finite automation]</ref> использовалась дополнительная структура, которую мы обозначим, как <tex>\mathtt{ClassInv}</tex>, в <tex>\mathtt{ClassInv}[C][a]</tex> будем хранить множество состояний, из которых есть ребро по символу <tex>a</tex> в состояние <tex>C</tex> (аналогично <tex>Inv</tex>, только для классов).
 
 
<tex>\mathtt{ClassInv}[C][a] = \{ s\ |\ \mathtt{Class}[s] == C  \ \land \  \delta^{-1} (s, a) \neq \emptyset \}</tex>
 
 
<tex>\mathtt{pushSetsToQueue}</tex> реализуем так:
 
 
  <tex>\mathtt{pushSetsToQueue}(\mathtt{Queue},\ R_1,\ R_2)</tex>:
 
      <tex>\mathrm{cnt1}  \leftarrow  \mathtt{ClassInv}[R_1][c]</tex>
 
      <tex>\mathrm{cnt2}  \leftarrow  \mathtt{ClassInv}[R_2][c]</tex>
 
      '''if''' <tex> \mathrm{cnt1} \leqslant \mathrm{cnt2} </tex>
 
        <tex>\mathtt{swapClasses}(R_1,\ R_2)</tex>
 
      '''for''' <tex>c \in \Sigma</tex>
 
        '''push''' <tex>\langle R_2, c \rangle</tex> '''to''' <tex>\mathtt{Queue}</tex>
 
 
Циклы
 
 
  '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
      (...)
 
 
реализуются так:
 
 
  '''for''' <tex>q \in \mathtt{ClassInv}[C][a]</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 
      (...)
 
 
Тогда время работы внутреннего цикла можно будет оценить как <tex>O(|\mathtt{ClassInv}[C][a]| + |\mathtt{Inverse}|)</tex>. А реализация <tex>\mathtt{pushSetsToQueue}</tex> выбирает множество, на котором <tex>O(|\mathtt{ClassInv}[C][a]|)</tex> будет меньшим.
 
 
Кроме того, вместо [[Хеш-таблица | хэш-таблиц]] для хранения множеств (<tex>\mathtt{ClassInv}</tex>, разбиение <tex>P</tex>) можно использовать комбинацию из двусвязного списка и вектора (добавление/удаление через список, поиск через вектор). Что и используется в оригинальной статье.
 
  
 
== См. также ==
 
== См. также ==
  
 
* [[Алгоритм Бржозовского]]
 
* [[Алгоритм Бржозовского]]
 
== Примечания ==
 
 
<references/>
 
  
 
== Источники информации ==
 
== Источники информации ==

Текущая версия на 21:50, 10 марта 2018

Пусть дан автомат, распознающий определенный язык. Требуется найти эквивалентный автомат с наименьшим количеством состояний.

Минимизация ДКА[править]

Если в ДКА существуют два эквивалентных состояния, то при их объединении мы получим эквивалентный ДКА, так как распознаваемый язык не изменится. Основная идея минимизации состоит в разбиении множества состояний на классы эквивалентности, полученные классы и будут состояниями минимизированного ДКА.

Простой алгоритм[править]

Определение:
Класс [math]C[/math] разбивает класс [math]R[/math] по символу [math]a[/math] на [math]R_1[/math] и [math]R_2[/math], если
  1. [math]\forall r \in R_1 \,\,\, \delta(r, a) \in C[/math]
  2. [math]\forall r \in R_2 \,\,\, \delta(r, a) \notin C[/math]

Если класс [math]R[/math] может быть разбит по символу [math]a[/math], то он содержит хотя бы одну пару неэквивалентных состояний (так как существует строка которая их различает). Если класс нельзя разбить, то он состоит из эквивалентных состояний. Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно.

Итеративно строим разбиение множества состояний следующим образом.

  1. Первоначальное разбиение множества состояний — класс допускающих состояний [math]F[/math] и класс недопускающих состояний ([math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]).
  2. Перебираются символы алфавита [math]c \in \Sigma[/math], все пары [math]\langle F,\ c \rangle[/math] и [math]\langle Q \setminus F, c \rangle[/math] помещаются в очередь.
  3. Из очереди извлекается пара [math]\langle C,\ a \rangle[/math], [math]C[/math] далее именуется как сплиттер.
  4. Каждый класс [math]R[/math] текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу [math]a[/math] переходят в сплиттер [math](R_1)[/math], а второй из всех оставшихся [math](R_2)[/math].
  5. Если [math]R[/math] разбился на два непустых подкласса (то есть [math] R_1 \ne \emptyset \ \land \ R_2 \ne \emptyset [/math]).
    1. В разбиении [math]P[/math] класс [math]R[/math] заменяется на свои подклассы [math]R_1[/math] и [math]R_2[/math].
    2. Перебираются символы алфавита [math]c \in \Sigma[/math], все пары [math]\langle R_1, c \rangle[/math] и [math]\langle R_2, c \rangle[/math] помещаются в очередь.
  6. Пока очередь не пуста, выполняем п.3 – п.5.

Псевдокод[править]

  • [math]\mathtt{Q}[/math] — множество состояний ДКА,
  • [math]\mathtt{F}[/math] — множество терминальных состояний,
  • [math]\mathtt{\delta}[/math] — функция перехода ([math]\delta (r,\ a)[/math] — состояние, в которое можно совершить переход из [math]r[/math] по символу [math]a[/math]),
  • [math]\mathtt{S}[/math] — очередь пар [math]\langle C,\ a \rangle[/math],
  • [math]\mathtt{P}[/math] — разбиение множества состояний ДКА,
  • [math]\mathtt{R}[/math] — класс состояний ДКА.
 function findEquivalenceClasses[math](Q,\ F,\ \delta)[/math]: vector
   [math]\mathtt{P} \leftarrow \{ F,\ Q \setminus F \}[/math]
   [math]\mathtt{S} \leftarrow \varnothing [/math]
   for [math]c \in \Sigma[/math]
     push [math]\langle F,\ c \rangle[/math], [math]\langle Q \setminus F,\ c \rangle[/math] into [math] \mathtt{S}[/math]
   while [math]\mathtt{S} \ne \varnothing[/math]
     [math]\langle C,\ a \rangle[/math] [math]\leftarrow[/math] pop from [math]\mathtt{S}[/math]
     for [math]R[/math] in [math]\mathtt{P}[/math] 
       [math] R_1, R_2 \leftarrow [/math] [math]\mathtt{split}(R,\ C,\ a)[/math]
       if [math] R_1 \ne \varnothing [/math] and [math] R_2 \ne \varnothing [/math]
         replace [math]R[/math] in [math]\mathtt{P}[/math] with [math]R_1[/math] and [math]R_2[/math]
         for [math]c \in \Sigma[/math]
           insert [math]\langle R_1,\ c \rangle[/math] in [math]\mathtt{S}[/math]
           insert [math]\langle R_2,\ c \rangle[/math] in [math]\mathtt{S}[/math]
   return [math]\mathtt{P}[/math]

Когда очередь [math]S[/math] станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить.

Время работы[править]

Время работы алгоритма оценивается как [math]O(|\Sigma| \cdot n^2)[/math], где [math] n [/math] — количество состояний ДКА, а [math] \Sigma [/math] — алфавит. Это следует из того, что если пара [math]\langle C,\ a \rangle[/math] попала в очередь, и класс [math]C[/math] использовался в качестве сплиттера, то при последующем разбиении этого класса в очередь добавляется два класса [math]C_1[/math] и [math]C_2[/math], причем можно гарантировать лишь следующее уменьшение размера: [math]|C| \geqslant |C_i| + 1[/math]. Каждое состояние изначально принадлежит лишь одному классу в очереди, поэтому каждый переход в автомате будет просмотрен не более, чем [math]O(n)[/math] раз. Учитывая, что ребер всего [math]O(|\Sigma| \cdot n)[/math], получаем указанную оценку.

Алгоритм Хопкрофта[править]

Рассмотрим алгоритм, позволяющий решить задачу быстрее, чем за [math] O(n^2) [/math].

Лемма:
Класс [math]R = R_1 \cup R_2[/math] и [math]R_1 \cap R_2 = \varnothing[/math], тогда разбиение всех классов (текущее разбиение) по символу [math]a[/math] любыми двумя классами из [math]R, R_1, R_2[/math] эквивалентно разбиению всех классов с помощью [math]R, R_1, R_2[/math] по символу [math]a[/math].
Доказательство:
[math]\triangleright[/math]

Разобьем все классы с помощью [math]R [/math] и [math] R_1[/math] по символу [math]a[/math], тогда для любого класса [math]B[/math] из текущего разбиения выполняется

[math]\forall r \in B \,\,\, \delta(r, a) \in R \ \land \ \delta(r, a) \in R_1 \ \lor[/math]
[math]\forall r \in B \,\,\, \delta(r, a) \in R \ \land \ \delta(r, a) \notin R_1 \ \lor[/math]
[math]\forall r \in B \,\,\, \delta(r, a) \notin R \ \land \ \delta(r, a) \notin R_1[/math]

А так как [math]R = R_1 \cup R_2[/math] и [math]R_1 \cap R_2 = \varnothing[/math] то выполняется

[math]\forall r \in B \,\,\, \delta(r, a) \in R_2 \ \lor[/math]
[math] \forall r \in B \,\,\, \delta(r, a) \notin R_2[/math]

Из этого следует, что разбиение всех классов с помощью [math]R_2[/math] никак не повлияет на текущее разбиение.
Аналогично доказывается и для разбиения с помощью [math]R [/math] и [math] R_2[/math] по символу [math]a[/math].
Разобьем все классы с помощью [math]R_1[/math] и [math] R_2[/math] по символу [math]a[/math], тогда для любого класса [math]B[/math] из текущего разбиения выполняется

[math]\forall r \in B \,\,\, \delta(r, a) \in R_1 \ \land \ \delta(r, a) \notin R_2 \ \lor[/math]
[math]\forall r \in B \,\,\, \delta(r, a) \notin R_1 \ \land \ \delta(r, a) \in R_2 \ \lor[/math]
[math]\forall r \in B \,\,\, \delta(r, a) \notin R_1 \ \land \ \delta(r, a) \notin R_2[/math]

А так как [math]R = R_1 \cup R_2[/math] и [math]R_1 \cap R_2 = \varnothing[/math] то выполняется

[math]\forall r \in B \,\,\, \delta(r, a) \in R \ \lor[/math]
[math]\forall r \in B \,\,\, \delta(r, a) \notin R[/math]
Из этого следует, что разбиение всех классов с помощью [math]R[/math] никак не повлияет на текущее разбиение.
[math]\triangleleft[/math]

Алгоритм Хопкрофта отличается от простого тем, что иначе добавляет пары в очередь. После замены класса [math]R[/math] в разбиении [math]P[/math] на его подклассы [math]R_1[/math] и [math]R_2[/math], как и раньше перебираем символы алфавита [math]c \in \Sigma[/math].

Если пара [math]\langle R,\ c \rangle[/math] уже есть в очереди, то согласно лемме можно просто заменить её на пары [math]\langle R_1, c \rangle[/math] и [math]\langle R_2, c \rangle[/math].

Если пары [math]\langle R,\ c \rangle[/math] нет в очереди, то достаточно добавить любую из пар [math]\langle R_1, c \rangle[/math] и [math]\langle R_2, c \rangle[/math]. Это следует из следующих соображений: [math]R[/math] может быть в разбиении только если в очередь были положены пары [math]\langle R,\ a \rangle[/math] для [math]\forall a \in \Sigma[/math], а поскольку в очереди пары [math]\langle R,\ c \rangle[/math] нет, то мы её уже успели рассмотреть, следовательно классы из разбиения [math]P[/math] уже были разбиты по [math]\langle R,\ c \rangle[/math].

Реализация[править]

  • [math]\mathtt{Q}[/math] — множество состояний ДКА,
  • [math]\mathtt{F}[/math] — множество терминальных состояний,
  • [math]\mathtt{\delta}[/math] — функция перехода ([math]\delta (r,\ a)[/math] — состояние, в которое можно совершить переход из [math]r[/math] по символу [math]a[/math]),
  • [math]\mathtt{S}[/math] — очередь пар [math]\langle C,\ a \rangle[/math],
  • [math]\mathtt{P}[/math] — разбиение множества состояний ДКА,
  • [math]\mathtt{R}[/math] — класс состояний ДКА.
 function findEquivalenceClasses[math](Q,\ F,\ \delta)[/math]: vector 
   [math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
   [math]\mathtt{S} \leftarrow \varnothing [/math]
   for [math]c \in \Sigma[/math]
     push [math]\langle F,\ c \rangle[/math], [math]\langle Q \setminus F,\ c \rangle[/math] into [math] \mathtt{S}[/math]
   while [math]\mathtt{S} \ne \varnothing[/math]
     [math]\langle C,\ a \rangle[/math] [math]\leftarrow[/math] pop from [math]\mathtt{S}[/math]
     for [math]R[/math] in [math]\mathtt{P}[/math] 
       [math] R_1, R_2 \leftarrow [/math] [math]\mathtt{split}(R,\ C,\ a)[/math]
       if [math] R_1 \ne \varnothing [/math] and [math] R_2 \ne \varnothing [/math] 
        replace [math]R[/math] in [math]\mathtt{P}[/math] with' [math]R_1[/math] and [math]R_2[/math]
        if [math]\langle R,\ c \rangle[/math] in [math] \mathtt{S}[/math] // смотрим, есть ли пара [math]\langle R,\ c \rangle[/math] в очереди 
          remove [math]\langle R, c \rangle[/math] from [math]\mathtt{S}[/math] // заменяем её на пары [math]\langle R_1, c \rangle[/math], [math]\langle R_2, c \rangle[/math] если пара есть 
          push [math]\langle R_1, c \rangle[/math] into [math]\mathtt{S}[/math]
          push [math]\langle R_2, c \rangle[/math] into [math]\mathtt{S}[/math]
        else
           if [math] |\mathtt{P}[R_1]| \leqslant |\mathtt{P}[R_2]| [/math] // вставляем любую иначе
             push [math]\langle R_1, c \rangle[/math] into [math]\mathtt{S}[/math]
           else
             push [math]\langle R_2, c \rangle[/math] into [math]\mathtt{S}[/math]
   return [math]\mathtt{P}[/math]



Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за [math]T'[/math] (его нужно будет эффективно находить для каждой пары [math]\langle C,\ a \rangle[/math]).

 function findEquivalenceClasses[math](Q,\ F,\ \delta)[/math]: vector 
   [math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
   [math]\mathtt{S} \leftarrow \varnothing [/math]
   for [math]c \in \Sigma[/math]
     push [math]\langle F,\ c \rangle[/math], [math]\langle Q \setminus F,\ c \rangle[/math] into [math] \mathtt{S}[/math]
   while [math]\mathtt{S} \ne \varnothing[/math]
     [math]\langle C,\ a \rangle[/math] [math]\leftarrow[/math] pop from [math]\mathtt{S}[/math]
     [math]\mathtt{Inverse} \leftarrow \{r \ | \ r \in Q, \ \delta(r, a) \in C\}[/math]
     [math]T' \leftarrow \{R \ | \ R \in \mathtt{P}, \ R \cap \mathtt{Inverse} \neq \varnothing\}[/math] // находим классы, из состояний которых есть ребро в состояния сплиттера 
     for [math]R[/math] in [math]T'[/math] // перебираем только классы входящие в [math]T'[/math]
       [math] R_1, R_2 \leftarrow [/math] [math]\mathtt{split}(R,\ C,\ a)[/math]
       if [math] R_1 \ne \varnothing [/math] and [math] R_2 \ne \varnothing [/math]
        replace [math]R[/math] in [math]\mathtt{P}[/math] with [math]R_1[/math] and [math]R_2[/math]
        if [math]\langle R,\ c \rangle[/math] in [math] \mathtt{S}[/math]
          remove [math]\langle R, c \rangle[/math] from [math]\mathtt{S}[/math]
          push [math]\langle R_1, c \rangle[/math] into [math]\mathtt{S}[/math]
          push [math]\langle R_2, c \rangle[/math] into [math]\mathtt{S}[/math]
        else
           if [math] |\mathtt{P}[R_1]| \leqslant |\mathtt{P}[R_2]| [/math]
             push [math]\langle R_1, c \rangle[/math] into [math]\mathtt{S}[/math]
           else
             push [math]\langle R_2, c \rangle[/math] into [math]\mathtt{S}[/math]
   return [math]\mathtt{P}[/math]


Каждая итерация цикла [math] \mathrm{while} [/math] может быть выполнена за [math] O(|Q| + |\mathtt{Inverse}|)\,[/math] для текущей пары [math]\langle C,\ a \rangle[/math]. Покажем, как можно достичь этой оценки.

Классы разбиения [math]P[/math] будем поддерживать с помощью множеств на хэш-таблицах (само же разбиение — обычный вектор, индекс — номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за [math]O(1)[/math]).

  • [math]\mathtt{Class}[r][/math] — номер класса, которому принадлежит состояние [math]r[/math],
  • [math]\mathtt{Queue}[/math] — очередь пар [math]\langle C,\ a \rangle[/math], где [math]C[/math] — номер класса (сплиттера),
  • [math]\mathtt{Inv}[r][a][/math] — массив состояний, из которых есть ребра по символу [math]a[/math] в состояние [math]r[/math] (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма).

Для обработки [math]T'[/math] за [math]O(|Q| + |\mathtt{Inverse}|)\,[/math] нам понадобится следующая структура:

  • [math]\mathtt{Involved}[/math] — список из номеров классов, содержащихся во множестве [math]T'[/math],
  • [math]\mathtt{Count}[/math] — целочисленный массив, где [math]\mathtt{Count}[i][/math] хранит количество состояний из класса [math]i[/math], которые содержатся в [math]\mathtt{Inverse}[/math],
  • [math]\mathtt{Twin}[/math] — массив, хранящий в [math]\mathtt{Twin}[i][/math] номер нового класса, образовавшегося при разбиении класса [math]i[/math].
 function findEquivalenceClasses[math](Q,\ F,\ \delta)[/math]: vector 
    [math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
    for [math]c \in \Sigma[/math]
      push [math]\langle F,\ c \rangle[/math], [math]\langle Q \setminus F,\ c \rangle[/math] into [math] \mathtt{Queue}[/math]
    while [math]\mathtt{Queue} \ne \varnothing[/math]
      [math]\langle C,\ a \rangle[/math] [math]\leftarrow[/math] pop from [math]\mathtt{Queue}[/math]
      [math]\mathtt{Involved} \leftarrow \varnothing[/math]
      for [math]q \in C[/math] and [math]r \in \mathtt{Inv}[q][a][/math]
        [math]i = \mathtt{Class}[r][/math]
        if [math]\mathtt{Count}[i] == 0[/math]
          insert [math]i[/math] into [math]\mathtt{Involved}[/math]
        [math]\mathtt{Count}[i]++[/math]
      for [math] i \in \mathtt{Involved}[/math]
        if [math]\mathtt{Count}[i] \lt  |\mathtt{P}[i]|[/math]
            insert [math]\{\}[/math] into [math]\mathtt{P}[/math] // создадим пустой класс в разбиении [math]\mathtt{P}[/math]
            [math]\mathtt{Twin}[i] = |\mathtt{P}|[/math]  //запишем в [math]\mathtt{Twin[i]}[/math] индекс нового класса
      for [math]q \in C[/math] and [math]r \in \mathtt{Inv}[q][a][/math]
        [math]i = \mathtt{Class}[r][/math]
        [math]j = \mathtt{Twin}[i][/math]
        if [math]j \neq 0[/math]
           remove [math]r[/math] from [math]\mathtt{P}[i][/math]
           add [math]r[/math] to [math]\mathtt{P}[j][/math]
      for [math] i \in \mathtt{Involved}[/math]
        [math]j = \mathtt{Twin}[i][/math]
        if [math] j \neq 0 [/math]
          if [math]|\mathtt{P}[j]| \gt  |\mathtt{P}[i]|[/math]  // парный класс должен быть меньшего размера
            [math]\mathtt{swap}(\mathtt{P}[i],\ \mathtt{P}[j])[/math] // swap за [math]\mathtt{O(1)}[/math] — просто переставить указатели
          for [math]r \in \mathtt{P}[j][/math]  // обновляем номера классов для вершин, у которых они изменились
            [math]\mathtt{Class}[r] = j[/math]
          for [math]c \in \Sigma[/math]
            push [math]\langle j, c \rangle[/math] to [math]\mathtt{Queue}[/math]
        [math]\mathtt{Count}[i] = 0[/math]
        [math]\mathtt{Twin}[i] = 0[/math]
    return [math]\mathtt{P}[/math]


Стоит отметить, что массивы [math]\mathtt{Count},\ \mathtt{Twin}\,[/math] аллоцируются ровно один раз при инициализации алгоритма.

Также стоит отметить, что собственно наличие/отсутствие пары в очереди можно не проверять. Если для некоторого [math]c[/math] пара [math]\langle i, c \rangle[/math] уже была в очереди, то мы добавим её "вторую половинку" [math]\langle \mathtt{Twin}[i], c \rangle[/math]. Если её в очереди не было, то мы вольны сами выбирать, какой подкласс добавлять в очередь, и таким образом добавляем опять же [math]\langle \mathtt{Twin}[i], c \rangle[/math]. Кроме того, вместо очереди можно использовать вообще произвольную структуру, хранящую элементы, в том числе стэк, множество, так как порядок извлечения нам по сути не важен.

Время работы[править]

Лемма (1):
Количество классов, созданных во время выполнения алгоритма, не превышает [math]2 |Q| - 1[/math].
Доказательство:
[math]\triangleright[/math]
Представим дерево, которое соответствует операциям разделения классов на подклассы. Корнем этого дерева является все множество состояний [math]Q[/math]. Листьями являются классы эквивалентности, оставшиеся после работы алгоритма. Так как дерево бинарное — каждый класс может породить лишь два новых, а количество листьев не может быть больше [math]|Q|[/math], то количество узлов этого дерева не может быть больше [math]2 |Q| - 1[/math], что доказывает утверждение леммы.
[math]\triangleleft[/math]
Лемма (2):
Количество итераций цикла [math]\mathrm{while}[/math] не превышает [math] 2 |\Sigma| |Q| [/math].
Доказательство:
[math]\triangleright[/math]

Для доказательства этого утверждения достаточно показать, что количество пар [math]\langle C,\ a \rangle[/math] добавленных в очередь [math]S[/math] не превосходит [math] 2 |\Sigma| |Q| [/math], так как на каждой итерации мы извлекаем одну пару из очереди.

По лемме(1) количество классов не превосходит [math]2 |Q| - 1[/math]. Пусть [math]C[/math] элемент текущего разбиения. Тогда количество пар [math]\langle C,\ a \rangle[/math], [math]\ a \in \Sigma[/math] не может быть больше [math]|\Sigma|[/math]. Отсюда следует, что всего различных пар, которые можно добавить в очередь, не превосходит [math] 2 |\Sigma| |Q| [/math].
[math]\triangleleft[/math]
Лемма (3):
Пусть [math]a \in \Sigma[/math] и [math]p \in Q[/math]. Тогда количество пар [math]\langle C,\ a \rangle[/math], где [math]p \in C[/math], которые мы удалим из очереди, не превосходит [math]\log_2(|Q|)[/math] для фиксированных [math]a[/math] и [math]p[/math].
Доказательство:
[math]\triangleright[/math]
Рассмотрим пару [math]\langle C,\ a \rangle[/math], где [math]p \in C[/math], которую мы удаляем из очереди. И пусть [math]\langle C',a \rangle[/math] следующая пара, где [math]p \in C'[/math] и которую мы удалим из очереди. Согласно нашему алгоритму класс [math]C'[/math] мог появиться в очереди только после операции [math]\mathtt{replace}[/math]. Но после первого же разбиения класса [math]C[/math] на подклассы мы добавим в очередь пару [math]\langle C'', a \rangle[/math], где [math]C''[/math] меньший из образовавшихся подклассов, то есть [math]|C''| \leqslant |C| \ / \ 2[/math]. Так же заметим, что [math]C' \subseteq C''[/math], а следовательно [math]|C'| \leqslant |C| \ / \ 2[/math]. Но тогда таких пар не может быть больше, чем [math]\log_2(|Q|)[/math].
[math]\triangleleft[/math]
Лемма (4):
[math]\sum |\mathtt{Inverse}|[/math] по всем итерациям цикла [math]\mathrm{while}[/math] не превосходит [math]|\Sigma| |Q| \log_2(|Q|)[/math].
Доказательство:
[math]\triangleright[/math]
Пусть [math]x, y \in Q[/math], [math]a \in \Sigma[/math] и [math] \delta(x, a) = y[/math]. Зафиксируем эту тройку. Заметим, что количество раз, которое [math]x[/math] встречается в [math]\mathtt{Inverse}\,[/math] при условии, что [math] \delta(x, a) = y[/math], совпадает с числом удаленных из очереди пар [math]\langle C,\ a \rangle[/math], где [math]y \in C[/math]. Но по лемме(3) эта величина не превосходит [math]\log_2(|Q|)[/math]. Просуммировав по всем [math] x \in Q [/math] и по всем [math] a \in \Sigma[/math] мы получим утверждение леммы.
[math]\triangleleft[/math]
Теорема:
Время работы алгоритма Хопкрофта равно [math]O(|\Sigma| |Q| \log(|Q|)[/math].
Доказательство:
[math]\triangleright[/math]

Оценим, сколько времени занимает каждая часть алгоритма:

  • Построение массива [math]\mathtt{Inv}[/math] занимает [math]O(|\Sigma| |Q|)[/math] времени.
  • По второй лемме количество итераций цикла [math]\mathrm{while}[/math] не превосходит [math]O(|\Sigma| |Q|)[/math].
  • Операции с множеством [math]T'[/math] и разбиение классов на подклассы требуют [math]O(\sum(|\mathtt{Inverse}|))\,[/math] времени. Но по лемме(4) [math]\sum(|\mathtt{Inverse}|)\,[/math] не превосходит [math]|\Sigma| |Q| \log_2(|Q|)[/math], то есть данная часть алгоритма выполняется за [math]O(|\Sigma| |Q| \log_2(|Q|))[/math].
  • В лемме(1) мы показали, что в процессе работы алгоритма не может появится больше, чем [math]2 |Q| - 1[/math] классов, из чего следует, что количество операций [math]\mathtt{replace}[/math] равно [math]O(|\Sigma| |Q|)[/math].
Итого, получается, что время работы алгоритма Хопкрофта не превышает [math] O(|\Sigma| |Q|) + O(|\Sigma| |Q|) + O(|\Sigma| |Q| \log_2(|Q|)) + O(|\Sigma| |Q|) = O(|\Sigma| |Q| \log_2(|Q|))[/math].
[math]\triangleleft[/math]

Альтернативная реализация[править]

Вообще, алгоритм можно реализовать и с меньшим количеством используемых структур (что делает код на порядок читабельнее).

Все классы разбиения будем по-прежнему хранить в векторе хэш-сетов [math]\mathtt{P}[/math].

  • [math]\mathtt{Class}[r][/math] — индекс класса в [math]\mathtt{P}[/math], которому принадлежит состояние [math]r[/math],
  • [math]\mathtt{Queue}[/math] — очередь из пар [math]\langle C,\ a \rangle[/math],
  • [math]\mathtt{Inv}[r][a][/math] — массив состояний, из которых есть ребра по символу [math]a[/math] в состояние [math]r[/math] (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма),
  • [math]\mathtt{Involved}[/math] — ассоциативный массив из номеров классов в векторы из номеров вершин.
 [math]\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)[/math]:
    [math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
    for [math]c \in \Sigma[/math]
      insert [math]\langle F,\ c \rangle[/math], [math]\langle Q \setminus F,\ c \rangle[/math] into [math] \mathtt{Queue}[/math]
    while [math]\mathtt{Queue} \ne \varnothing[/math]
      [math]\langle C,\ a \rangle[/math] [math]\leftarrow[/math] pop from [math]\mathtt{Queue}[/math]
      [math]\mathtt{Involved} = \{\}[/math]
      for [math]q \in C[/math] and [math]r \in \mathtt{Inv}[q][a][/math]
        [math]i = \mathtt{Class}[r][/math]
        if [math]\mathtt{Involved}[i] == \varnothing[/math]
            [math]\mathtt{Involved}[i] = \{\}[/math]
        add [math]r[/math] to [math]\mathtt{Involved}[i][/math]
      for [math] i \in \mathtt{Involved}[/math] //Перебираем ключи [math]\mathtt{Involved}[/math]
        if [math]|\mathtt{Involved}[i]| \lt  |\mathtt{P}[i]|[/math]
            insert [math]\{\}[/math] into [math]\mathtt{P}[/math] //Создадим пустой класс в разбиении [math]\mathtt{P}[/math]
            [math]j = |\mathtt{P}|[/math] //Запишем в [math]j[/math] индекс нового класса
            for [math]r[/math] in [math]\mathtt{Involved}[i][/math]
              remove [math]r[/math] from [math]\mathtt{P}[i][/math]
              add [math]r[/math] to [math]\mathtt{P}[j][/math]
            if [math]|\mathtt{P}[j]| \gt  |\mathtt{P}[i]|[/math]  //Парный класс должен быть меньшего размера
               [math]\mathtt{swap}(\mathtt{P}[i],\ \mathtt{P}[j])[/math] //swap за [math]\mathtt{O(1)}[/math] — просто переставить указатели
            for [math]r \in \mathtt{P}[j][/math] //Обновляем номера классов для вершин, у которых они изменились
              [math]\mathtt{Class}[r] = j[/math]
            for [math]c \in \Sigma[/math]
              push [math]\langle j, c \rangle[/math] into [math]\mathtt{Queue}[/math]
    return [math]\mathtt{P}[/math]

См. также[править]

Источники информации[править]

  • Хопкрофт Д., Мотвани Р., Ульман Д. Введение в теорию автоматов, языков и вычислений, 2-е изд. : Пер. с англ. — М.: Издательский дом «Вильямс», 2002. — С. 177 — ISBN 5-8459-0261-4 (рус.)
  • D. Gries. Describing an algorithm by Hopcroft. Technical Report TR-72-151, Cornell University, December 1972.
  • Hang Zhou. Implementation of Hopcroft's Algorithm, 19 December 2009.
  • John Hopcroft An O(nlogn) algorithm for minimizing states in a finite automation