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

Материал из Викиконспекты
Перейти к: навигация, поиск
(Алгоритм Хопкрофта)
(Тикет 2-17)
Строка 15: Строка 15:
  
 
Итеративно строим разбиение множества состояний следующим образом.
 
Итеративно строим разбиение множества состояний следующим образом.
# Первоначальное разбиение множества состояний {{---}} класс допускающих состояний <tex>F</tex> и класс недопускающих состояний <tex>Q \setminus F</tex>.
+
# Первоначальное разбиение множества состояний {{---}} класс допускающих состояний <tex>F</tex> и класс недопускающих состояний (<tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex>).
 
# Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>(F, c)</tex> и <tex>(Q \setminus F, c)</tex> помещаются в очередь.
 
# Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>(F, c)</tex> и <tex>(Q \setminus F, c)</tex> помещаются в очередь.
# Из очереди извлекается пара <tex>(C, a)</tex>, <tex>C</tex> далее именуется как мастер Сплиттер.
+
# Из очереди извлекается пара <tex>(C, a)</tex>, <tex>C</tex> далее именуется как сплиттер.
# Все классы текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>a</tex> переходят в сплиттер, а второй из всех оставшихся.  
+
# Каждый класс <tex>R</tex> текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>a</tex> переходят в сплиттер (<tex>R_1</tex>), а второй из всех оставшихся (<tex>R_2</tex>).  
# Те классы, которые разбились на два непустых подкласса, заменяются этими подклассами в разбиении, а подклассы добавляются в очередь.
+
# Если <tex>R</tex> разбился на два непустых подкласса (т.е. <tex> R_1 \ne \emptyset</tex> and <tex>R_2 \ne \emptyset </tex>).
 +
## В разбиении <tex>P</tex> класс <tex>R</tex> заменяется на свои подклассы <tex>R_1</tex> и <tex>R_2</tex>.
 +
## Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>(R_1, c)</tex> и <tex>(R_2, c)</tex> помещаются в очередь.
 
# Пока очередь не пуста, выполняем п.3 – п.5.
 
# Пока очередь не пуста, выполняем п.3 – п.5.
  
Строка 28: Строка 30:
 
<tex>P</tex> {{---}} разбиение множества состояний ДКА.
 
<tex>P</tex> {{---}} разбиение множества состояний ДКА.
 
<tex>R</tex> {{---}} класс состояний ДКА.
 
<tex>R</tex> {{---}} класс состояний ДКА.
 +
 
   <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex>
 
   <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex>
 
   <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
   <tex>\mathtt{S} \leftarrow \varnothing </tex>
Строка 33: Строка 36:
 
     '''insert''' <tex>(\mathtt{F}, c)</tex> '''in''' <tex>\mathtt{S}</tex>
 
     '''insert''' <tex>(\mathtt{F}, c)</tex> '''in''' <tex>\mathtt{S}</tex>
 
     '''insert''' <tex>(\mathtt{Q} \setminus \mathtt{F}, c)</tex> '''in''' <tex>\mathtt{S}</tex>
 
     '''insert''' <tex>(\mathtt{Q} \setminus \mathtt{F}, c)</tex> '''in''' <tex>\mathtt{S}</tex>
   '''while''' <tex> \mathtt{S} \ne \varnothing </tex>
+
   '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
     <tex>(C, a) \leftarrow</tex> '''pop'''(<tex>\mathtt{S}</tex>)
+
     <tex>(C, a) \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 \cap \delta^{-1} (C, a) </tex>
+
       <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</tex>)
      <tex>R_2 = R \setminus R_1</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>(R_1, c)</tex> '''in''' <tex>\mathtt{S}</tex>
+
        '''insert''' <tex>(R_1, c)</tex> '''in''' <tex>\mathtt{S}</tex>
          '''insert''' <tex>(R_2, c)</tex> '''in''' <tex>\mathtt{S}</tex>
+
        '''insert''' <tex>(R_2, c)</tex> '''in''' <tex>\mathtt{S}</tex>
 
Когда очередь <tex>S</tex> станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить.
 
Когда очередь <tex>S</tex> станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить.
  
Строка 49: Строка 51:
  
 
== Алгоритм Хопкрофта==
 
== Алгоритм Хопкрофта==
 +
Рассмотрим алгоритм, позволяющий решить задачу быстрее, чем за <tex> O(n^2) </tex>.
  
 
{{Лемма
 
{{Лемма
Строка 72: Строка 75:
 
}}
 
}}
  
Алгоритм Хопкрофта отличается от простого тем, что иначе добавляет классы в очередь.
+
Алгоритм Хопкрофта отличается от простого тем, что иначе добавляет пары в очередь.
Если класс <tex>R</tex> уже есть в очереди, то согласно лемме можно просто заменить его на <tex>R_1</tex> и <tex>R_2</tex>.  
+
После замены класса <tex>R</tex> в разбиении <tex>P</tex> на его подклассы <tex>R_1</tex> и <tex>R_2</tex>, как и раньше перебираем символы алфавита <tex>c \in \Sigma</tex>.
Если класса <tex>R</tex> нет в очереди, то согласно лемме в очередь можно добавить класс <tex>R</tex> и любой из <tex>R_1</tex> и <tex>R_2</tex>, а так как для любого класса <tex>B</tex> из текущего разбиения выполняется
+
 
:<tex>\forall r \in B \,\,\, \delta(r, a) \in R </tex> or
+
Если пара <tex>(R, c)</tex> уже есть в очереди, то согласно лемме можно просто заменить её на пары <tex>(R_1, c)</tex> и <tex>(R_2, c)</tex>.
:<tex> \forall r \in B \,\,\, \delta(r, a) \notin R</tex>  
+
 
то в очередь можно добавить только меньшее из <tex>R_1</tex> и <tex>R_2</tex>.
+
Если пары <tex>(R, c)</tex> нет в очереди, то достаточно добавить любую из пар <tex>(R_1, c)</tex> и <tex>(R_2, c)</tex>. Это следует из следующих соображений: <tex>R</tex> может быть в разбиении только если в очередь были положены пары <tex>(R, a)</tex> для <tex>\forall a \in \Sigma</tex>, а поскольку в очереди пары <tex>(R, c)</tex> нет, то  мы её уже успели рассмотреть, следовательно классы из разбиения <tex>P</tex> уже были разбиты по <tex>(R, c)</tex>.
 +
 
 +
=== Реализация ===
 +
<tex>pushSetsToQueue(S, R_1, R_2, c)</tex> {{---}} функция, которая добавляет одно из <tex>(R_1, c)</tex>, <tex>(R_2, c)</tex> в очередь S.
  
===Реализация===
 
 
<tex>Q</tex> {{---}} множество состояний ДКА.
 
<tex>Q</tex> {{---}} множество состояний ДКА.
 
<tex>F</tex> {{---}} множество терминальных состояний.
 
<tex>F</tex> {{---}} множество терминальных состояний.
<tex>S</tex> {{---}} очередь из пар <tex>(C, a)</tex>.
+
<tex>S</tex> {{---}} очередь пар <tex>(C, a)</tex>.
 
<tex>P</tex> {{---}} разбиение множества состояний ДКА.
 
<tex>P</tex> {{---}} разбиение множества состояний ДКА.
 
<tex>R</tex> {{---}} класс состояний ДКА.
 
<tex>R</tex> {{---}} класс состояний ДКА.
Строка 89: Строка 94:
 
   <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
   <tex>\mathtt{S} \leftarrow \varnothing </tex>
 
   '''for''' <tex>c \in \Sigma</tex>
 
   '''for''' <tex>c \in \Sigma</tex>
     '''insert''' <tex> (\mathtt{min} (\mathtt{F, Q} \setminus \mathtt{F}), c)</tex> '''in''' <tex>\mathtt{S}</tex>
+
     <tex>pushSetsToQueue(S, F, Q \setminus F, c)</tex>
 
   '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
 
   '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
     <tex>(C, a) \leftarrow</tex> '''pop'''(<tex>\mathtt{S}</tex>)
+
     <tex>(C, a) \leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex>
    <tex>T \leftarrow \{R \ | \ R \in \mathtt{P}, \ R</tex> splits by <tex>(C, a) \}</tex>
+
     '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex>  
     '''for each''' <tex>R</tex> '''in''' <tex>T</tex>
+
       <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</tex>)
       <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</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>
        '''if''' <tex>(R, c)</tex> '''in''' <tex>\mathtt{S}</tex>
+
         <tex>pushSetsToQueue(S, R_1, R_2, c)</tex>
          '''replace''' <tex> (R, c)</tex> '''in''' <tex>\mathtt{S}</tex> '''with''' <tex>(R_1, c)</tex> '''and''' <tex>(R_2, c)</tex>
 
         '''else'''
 
          '''insert''' <tex>(\mathtt{min}(R_1, R_2), c)</tex> '''in''' <tex>\mathtt{S}</tex>
 
  
К сожалению, совсем не очевидно, как быстро находить множество <tex>T</tex>. С другой стороны, понятно, что <tex>T \subset T'</tex>, где <tex>T'</tex> {{---}} это множество классов текущего разбиения, из состояний которых в автомате существует переход в состояния сплиттера <tex>C</tex> по символу <tex>a</tex>.
+
Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за T' (его нужно будет эффективно находить для каждой пары <tex>(C, a)</tex>).
 
 
Модифицируем наш алгоритм: для каждой очередной пары <tex> (C, a) </tex> будем находить <tex> T' </tex>, и с каждым классом состояний из <tex> T' </tex> будем производить те же действия, что и раньше.
 
  
 
   <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex>
 
   <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{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>
     '''insert''' <tex>(\mathtt{min} (\mathtt{F, Q} \setminus \mathtt{F}), c)</tex> '''in''' <tex>\mathtt{S}</tex>
+
     <tex>pushSetsToQueue(S, F, Q \setminus F, c)</tex>
 
   '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
 
   '''while''' <tex>\mathtt{S} \ne \varnothing</tex>
     <tex>(C, a) \leftarrow</tex> '''pop'''(<tex>\mathtt{S}</tex>)
+
     <tex>(C, a) \leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex>
 
     <tex>\mathtt{Inverse} \leftarrow \{r \ | \ r \in \mathtt{Q}, \ \delta(r, a) \in C\}</tex>
 
     <tex>\mathtt{Inverse} \leftarrow \{r \ | \ r \in \mathtt{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>
     '''for each''' <tex>R</tex> '''in''' <tex>T'</tex>
+
     '''for''' <tex>R</tex> '''in''' <tex>T'</tex>  
       '''if''' <tex>R</tex> splits by <tex>(C, a)</tex>
+
       <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</tex>)
        <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</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>
          '''if''' <tex>(R, c)</tex> '''in''' <tex>\mathtt{S}</tex>
+
        <tex>pushSetsToQueue(S, R_1, R_2, c)</tex>
            '''replace''' <tex>(R, c)</tex> '''in''' <tex>S</tex> '''with''' <tex>(R_1, c)</tex> '''and''' <tex>(R_2, c)</tex>
 
          '''else'''
 
            '''insert''' <tex>(\mathtt{min}(R_1, R_2), c)</tex> '''in''' <tex>\mathtt{S}</tex>
 
  
Каждая итерация цикла <tex> while </tex> не может быть выполнена быстрее, чем за <tex> O(|\mathtt{Inverse}|) </tex> для текущей пары <tex> (C,a)</tex>. Покажем, как достичь этой оценки.
 
  
Разбиение <tex> P </tex> можно поддерживать четырьмя массивами:
+
Каждая итерация цикла <tex> \mathrm{while} </tex> может быть выполнена за <tex> O(|Q| + |\mathtt{Inverse}|) </tex> для текущей пары <tex> (C,a)</tex>. Покажем, как можно достичь этой оценки.
*<tex>\mathtt{Class}[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex>;
 
*<tex>\mathtt{Part}[i]</tex> {{---}} указатель на голову [[Список#Двусвязный список|двусвязного списка]], содержащего состояния, принадлежащие классу <tex> i </tex>;
 
*<tex>\mathtt{Card}[i]</tex> {{---}} количество состояний в классе <tex>i</tex>;
 
*<tex>\mathtt{Place}[r]</tex> {{---}} указатель на состояние <tex>r</tex> в списке <tex>\mathtt{Part[Class}[r]]</tex>.
 
  
Так как мы храним указатель, где находится состояние в двусвязном списке, то операцию перемещения состояния из одного класса в другой можно выполнить за <tex>O(1)</tex>.
+
Классы разбиения <tex>P</tex> будем поддерживать с помощью множеств на хэш-таблицах (само же разбиение - обычный вектор, индекс - номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за O(1)).
  
Чтобы эффективно находить множество <tex>\mathtt{Inverse}</tex>, построим массив <tex>\mathtt{Inv}</tex>, который для состояния <tex>r</tex> и символа <tex>a</tex> в <tex>\mathtt{Inv}[r][a]</tex> хранит множество состояний, из которых существует переход в <tex>r</tex> по символу <tex>a</tex>. Так как наш алгоритм не меняет изначальный автомат, то массив <tex>\mathtt{Inv}</tex> можно построить перед началом основной части алгоритма, что займет <tex>O(|\Sigma| |Q|)</tex> времени.
+
*<tex>\mathtt{Class}[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex>
 +
*<tex>\mathtt{Card}[i]</tex> {{---}} размер класса <tex>i</tex>
 +
*<tex>\mathtt{Queue}</tex> {{---}} очередь пар <tex>(C, a)</tex>, где <tex>C</tex> - номер класса (сплиттера)
 +
*<tex>\mathtt{InQueue}[C]</tex> {{---}} множество на хэш-таблице, содержащее символ <tex>a</tex>, если в очереди содержится пара <tex>(C, a)</tex>
 +
*<tex>\mathtt{Inv}[r][a]</tex> {{---}} массив состояний, из которых есть ребра по символу <tex>a</tex> в состояние <tex>r</tex> (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма)
  
Теперь научимся за <tex>O(|\mathtt{Inverse}|)</tex> обрабатывать множество <tex>T'</tex> и разбивать классы. Для этого нам понадобится следующая структура:
+
Для обработки <tex>T'</tex> за <tex>O(|Q| + |\mathtt{Inverse}|)</tex> нам понадобится следующая структура:
 
*<tex>\mathtt{Counter}</tex> {{---}} количество классов;
 
*<tex>\mathtt{Counter}</tex> {{---}} количество классов;
 
*<tex>\mathtt{Involved}</tex> {{---}} список из номеров классов, содержащихся во множестве <tex>T'</tex>;
 
*<tex>\mathtt{Involved}</tex> {{---}} список из номеров классов, содержащихся во множестве <tex>T'</tex>;
Строка 142: Строка 138:
 
*<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>T'</tex> будет выглядеть так:
+
  //Добавим <tex> F, Q \setminus F </tex> в <tex> \mathtt{Queue} </tex>, <tex>\mathtt{InQueue}</tex>
  <tex>\mathtt{Involved} \leftarrow \varnothing</tex>
+
  '''while''' <tex>\mathtt{Queue} \ne \varnothing</tex>
  '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
+
    <tex>(C, a) \leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{Queue}</tex>
    <tex>i = \mathtt{Class}[r]</tex>
+
    <tex>\mathtt{Involved} \leftarrow \varnothing</tex>
    '''if''' <tex>\mathtt{Size}[i] == 0</tex>
+
    '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
      '''insert''' <tex>i</tex> '''in''' <tex>\mathtt{Involved}</tex>
+
      <tex>i = \mathtt{Class}[r]</tex>
    <tex>\mathtt{Size}[i]++</tex>
+
      '''if''' <tex>\mathtt{Size}[i] == 0</tex>
  '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
+
        '''insert''' <tex>i</tex> '''in''' <tex>\mathtt{Involved}</tex>
    <tex>i = \mathtt{Class}[r]</tex>
+
      <tex>\mathtt{Size}[i]++</tex>
    '''if''' <tex>\mathtt{Size}[i] < \mathtt{Card}[i]</tex>
+
    '''for''' <tex> i \in \mathtt{Involved}</tex>
      '''if''' <tex>\mathtt{Twin}[i] == 0</tex>
+
    '''if''' <tex>\mathtt{Size}[i] < \mathtt{Card}[i]</tex>
        <tex>\mathtt{Counter}++</tex>
+
    <tex>\mathtt{Size}[i] = -1</tex> //Пометим сразу, т.к. в следующем цикле классы уже будут менятся
        <tex>\mathtt{Twin[i]} = \mathtt{Counter}</tex>
+
    '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
      '''move''' <tex>r</tex> '''from''' <tex>i</tex> '''to''' <tex>\mathtt{Twin}[i]</tex>
+
      <tex>i = \mathtt{Class}[r]</tex>
  '''for''' <tex> j \in \mathtt{Involved}</tex>
+
      '''if''' <tex>\mathtt{Size}[i] == -1</tex>
    <tex>\mathtt{Size}[j] = 0</tex>
+
        '''if''' <tex>\mathtt{Twin}[i] == 0</tex>
    <tex>\mathtt{Twin}[j] = 0</tex>
+
          <tex>\mathtt{Counter}++</tex>
 +
          <tex>\mathtt{Twin[i]} = \mathtt{Counter}</tex>
 +
        '''move''' <tex>r</tex> '''from''' <tex>i</tex> '''to''' <tex>\mathtt{Twin}[i]</tex>
 +
    '''for''' <tex> j \in \mathtt{Involved}</tex>
 +
      '''if''' <tex> \mathtt{Twin}[j] \neq 0 </tex>
 +
          '''for''' <tex>c \in \Sigma</tex>
 +
            <tex>pushSetsToQueue(\mathtt{Queue}, j, \mathtt{Twin}[j], c)</tex>
 +
      <tex>\mathtt{Size}[j] = 0</tex>
 +
      <tex>\mathtt{Twin}[j] = 0</tex>
 +
    '''remove''' <tex>a</tex> '''from''' <tex>\mathtt{InQueue}[C]</tex>
 +
 
 +
Стоит отметить, что массивы <tex>\mathtt{Size}, \mathtt{Twin}</tex> аллоцируются ровно один раз при инициализации алгоритма.
  
Для быстрой проверки, находится ли пара <tex>(C,a)</tex> в очереди <tex>S</tex>, будем использовать массив <tex>\mathtt{InQueue}</tex> размера <tex>|Q| \times |\Sigma|</tex>, где <tex>\mathtt{InQueue}[C][a] = true</tex>, если пара <tex>(C,a)</tex> содержится в очереди.
+
Осталось только реализовать <tex>pushSetsToQueue</tex>.
Так как при разбиении очередного класса <tex>R</tex> на подклассы <tex>R_1</tex> и <tex>R_2</tex> мы в действительности создаем лишь один новый класс, то замена класса <tex>R</tex> в очереди на подклассы, образовавшиеся при разбиении, сводится лишь к взаимодействию с массивом <tex>\mathtt{InQueue}</tex>. В результате каждая операция с очередью требует <tex>O(1)</tex> времени.
+
  <tex>pushSetsToQueue(\mathtt{Queue}, R_1, R_2, c)</tex>:
 +
      <tex>cnt1  \leftarrow  \mathtt{Card}[R_1]</tex>  
 +
      <tex>cnt2  \leftarrow  \mathtt{Card}[R_2]</tex>  
 +
      '''if''' <tex> \lnot ( \mathtt{InQueue}[R_1] </tex> '''contains''' <tex> c) </tex> '''and''' <tex> cnt1 <= cnt2 </tex>
 +
        '''push''' <tex>(R_1, c)</tex> '''to''' <tex>\mathtt{Queue}</tex>
 +
        '''insert''' <tex> c </tex> '''into''' <tex>\mathtt{InQueue}[R_1]</tex>
 +
      '''else'''
 +
        '''push''' <tex>(R_2, c)</tex> '''to''' <tex>\mathtt{Queue}</tex>
 +
        '''insert''' <tex> c </tex> '''into''' <tex>\mathtt{InQueue}[R_2]</tex>
  
 
===Время работы===
 
===Время работы===
Строка 178: Строка 193:
 
|id = Лемма2
 
|id = Лемма2
 
|statement =  
 
|statement =  
Количество итераций цикла <tex>while</tex> не превышает <tex> 2 |\Sigma| |Q| </tex>.
+
Количество итераций цикла <tex>\mathrm{while}</tex> не превышает <tex> 2 |\Sigma| |Q| </tex>.
 
|proof =
 
|proof =
 
Для доказательства этого утверждения достаточно показать, что количество пар <tex>(C,a)</tex> добавленных в очередь <tex>S</tex> не превосходит <tex> 2 |\Sigma| |Q| </tex>, так как на каждой итерации мы извлекаем одну пару из очереди.
 
Для доказательства этого утверждения достаточно показать, что количество пар <tex>(C,a)</tex> добавленных в очередь <tex>S</tex> не превосходит <tex> 2 |\Sigma| |Q| </tex>, так как на каждой итерации мы извлекаем одну пару из очереди.
Строка 198: Строка 213:
 
|id = Лемма4
 
|id = Лемма4
 
|statement =  
 
|statement =  
<tex>\sum |\mathtt{Inverse}|</tex> по всем итерациям цикла <tex>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>(C, a)</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>(C, a)</tex>, где <tex>y \in C</tex>. Но по [[#Лемма3 | лемме(3)]] эта величина не превосходит <tex>\log_2(|Q|)</tex>. Просуммировав по всем <tex> x \in Q </tex> и по всем <tex> a \in \Sigma</tex> мы получим утверждение леммы.
Строка 211: Строка 226:
 
*Построение массива <tex>\mathtt{Inv}</tex> занимает <tex>O(|\Sigma| |Q|)</tex> времени.  
 
*Построение массива <tex>\mathtt{Inv}</tex> занимает <tex>O(|\Sigma| |Q|)</tex> времени.  
  
*По [[#Лемма2 | второй лемме]] количество итераций цикла <tex>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>.
Строка 219: Строка 234:
 
Итого, получается, что время работы алгоритма Хопкрофта не превышает <tex> O(|\Sigma| |Q|) + O(|\Sigma| |Q|) + O(|\Sigma| |Q| \log_2(|Q|)) + O(|\Sigma| |Q|) = O(|\Sigma| |Q| \log_2(|Q|))</tex>.
 
Итого, получается, что время работы алгоритма Хопкрофта не превышает <tex> O(|\Sigma| |Q|) + O(|\Sigma| |Q|) + O(|\Sigma| |Q| \log_2(|Q|)) + O(|\Sigma| |Q|) = O(|\Sigma| |Q| \log_2(|Q|))</tex>.
 
}}
 
}}
 +
 +
== Сравнение с алгоритмом из оригинальной статьи Хопкрафта ==
 +
 +
В [http://i.stanford.edu/pub/cstr/reports/cs/tr/71/190/CS-TR-71-190.pdf оригинальной статье] использовалась дополнительная структура, которую мы обозначим, как <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 </tex> and <tex> \delta^{-1} (s, a) \neq \emptyset \}</tex>
 +
 +
<tex>pushSetsToQueue</tex> реализуем так:
 +
 +
<tex>pushSetsToQueue(\mathtt{Queue}, R_1, R_2, c)</tex>:
 +
  <tex>cnt1  \leftarrow  \mathtt{ClassInv}[R_1][c]</tex>
 +
  <tex>cnt2  \leftarrow  \mathtt{ClassInv}[R_2][c]</tex>
 +
  '''if''' <tex> \lnot ( \mathtt{InQueue}[R_1] </tex> '''contains''' <tex> c) </tex> '''and''' <tex> cnt1 <= cnt2 </tex>
 +
    '''push''' <tex>(R_1, c)</tex> '''to''' <tex>\mathtt{Queue}</tex>
 +
    '''insert''' <tex> c </tex> '''into''' <tex>\mathtt{InQueue}[R_1]</tex>
 +
  '''else'''
 +
    '''push''' <tex>(R_2, c)</tex> '''to''' <tex>\mathtt{Queue}</tex>
 +
    '''insert''' <tex> c </tex> '''into''' <tex>\mathtt{InQueue}[R_2]</tex>
 +
 +
Циклы
 +
 +
'''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 +
<tex>(...)</tex>
 +
 +
реализуются
 +
 +
'''for''' <tex>q \in \mathtt{ClassInv}[C][a]</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex>
 +
<tex>(...)</tex>'
 +
 +
Тогда время работы внутреннего цикла можно будет оценить как <tex>O(|\mathtt{ClassInv}[C][a]| + |\mathtt{Inverse}|)</tex>. А реализация <tex>pushSetsToQueue</tex> выбирает множество, на котором <tex>O(|\mathtt{ClassInv}[C][a]|)</tex> будет меньшим.
 +
 +
Кроме того, вместо хэш-таблиц для хранения множеств (<tex>\mathtt{ClassInv}</tex>, разбиение <tex>P</tex>) можно использовать комбинацию из двусвязного списка и вектора (добавление/удаление через список, поиск через вектор). Что и используется в оригинальной статье.
  
 
== Литература ==
 
== Литература ==
Строка 224: Строка 271:
 
* ''D. Gries.'' Describing an algorithm by Hopcroft. Technical Report TR-72-151, Cornell University, December 1972.
 
* ''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.
 
* ''Hang Zhou.'' Implementation of Hopcroft's Algorithm, 19 December 2009.
 +
* [http://i.stanford.edu/pub/cstr/reports/cs/tr/71/190/CS-TR-71-190.pdf Оригинальная статья Хопкрофта]
  
 
[[Категория: Теория формальных языков]]
 
[[Категория: Теория формальных языков]]
 
[[Категория: Автоматы и регулярные языки]]
 
[[Категория: Автоматы и регулярные языки]]

Версия 16:31, 9 января 2015

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

Минимизация ДКА

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

Простой алгоритм

Определение:
Класс [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 \{ \mathtt{F, Q} \setminus \mathtt{F} \}[/math]).
  2. Перебираются символы алфавита [math]c \in \Sigma[/math], все пары [math](F, c)[/math] и [math](Q \setminus F, c)[/math] помещаются в очередь.
  3. Из очереди извлекается пара [math](C, a)[/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[/math] and [math]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](R_1, c)[/math] и [math](R_2, c)[/math] помещаются в очередь.
  6. Пока очередь не пуста, выполняем п.3 – п.5.

Псевдокод

[math]Q[/math] — множество состояний ДКА. [math]F[/math] — множество терминальных состояний. [math]S[/math] — очередь пар [math](C, a)[/math]. [math]P[/math] — разбиение множества состояний ДКА. [math]R[/math] — класс состояний ДКА.

 [math]\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}[/math]
 [math]\mathtt{S} \leftarrow \varnothing [/math]
 for [math]c \in \Sigma[/math]
   insert [math](\mathtt{F}, c)[/math] in [math]\mathtt{S}[/math]
   insert [math](\mathtt{Q} \setminus \mathtt{F}, c)[/math] in [math]\mathtt{S}[/math]
 while [math]\mathtt{S} \ne \varnothing[/math]
   [math](C, a) \leftarrow[/math] pop from [math]\mathtt{S}[/math]
   for [math]R[/math] in [math]\mathtt{P}[/math] 
     [math] R_1, R_2 \leftarrow [/math] split([math]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](R_1, c)[/math] in [math]\mathtt{S}[/math]
       insert [math](R_2, c)[/math] in [math]\mathtt{S}[/math]

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

Время работы

Время работы алгоритма оценивается как [math]O(|\Sigma| \cdot n^2)[/math], где [math] n [/math] — количество состояний ДКА, а [math] \Sigma [/math]— алфавит. Это следует из того, что если пара [math](C, a)[/math] попала в очередь, и класс [math]C[/math] использовался в качестве сплиттера, то при последующем разбиении этого класса в очередь добавляется два класса [math]C_1[/math] и [math]C_2[/math], причем можно гарантировать лишь следующее уменьшение размера: [math]|C| \ge |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[/math] and [math] \delta(r, a) \in R_1[/math] or
[math]\forall r \in B \,\,\, \delta(r, a) \in R[/math] and [math] \delta(r, a) \notin R_1[/math] or
[math]\forall r \in B \,\,\, \delta(r, a) \notin R[/math] and [math] \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 [/math] or
[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[/math] and [math] \delta(r, a) \notin R_2[/math] or
[math]\forall r \in B \,\,\, \delta(r, a) \notin R_1[/math] and [math] \delta(r, a) \in R_2[/math] or
[math]\forall r \in B \,\,\, \delta(r, a) \notin R_1[/math] and [math] \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 [/math] or
[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](R, c)[/math] уже есть в очереди, то согласно лемме можно просто заменить её на пары [math](R_1, c)[/math] и [math](R_2, c)[/math].

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

Реализация

[math]pushSetsToQueue(S, R_1, R_2, c)[/math] — функция, которая добавляет одно из [math](R_1, c)[/math], [math](R_2, c)[/math] в очередь S.

[math]Q[/math] — множество состояний ДКА. [math]F[/math] — множество терминальных состояний. [math]S[/math] — очередь пар [math](C, a)[/math]. [math]P[/math] — разбиение множества состояний ДКА. [math]R[/math] — класс состояний ДКА.

 [math]\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}[/math]
 [math]\mathtt{S} \leftarrow \varnothing [/math]
 for [math]c \in \Sigma[/math]
   [math]pushSetsToQueue(S, F, Q \setminus F, c)[/math]
 while [math]\mathtt{S} \ne \varnothing[/math]
   [math](C, a) \leftarrow[/math] pop from [math]\mathtt{S}[/math]
   for [math]R[/math] in [math]\mathtt{P}[/math] 
     [math] R_1, R_2 \leftarrow [/math] split([math]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]
       [math]pushSetsToQueue(S, R_1, R_2, c)[/math]

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

 [math]\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}[/math]
 [math]\mathtt{S} \leftarrow \varnothing [/math]
 for [math]c \in \Sigma[/math]
   [math]pushSetsToQueue(S, F, Q \setminus F, c)[/math]
 while [math]\mathtt{S} \ne \varnothing[/math]
   [math](C, a) \leftarrow[/math] pop from [math]\mathtt{S}[/math]
   [math]\mathtt{Inverse} \leftarrow \{r \ | \ r \in \mathtt{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] R_1, R_2 \leftarrow [/math] split([math]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]
       [math]pushSetsToQueue(S, R_1, R_2, c)[/math]


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

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

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

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

  • [math]\mathtt{Counter}[/math] — количество классов;
  • [math]\mathtt{Involved}[/math] — список из номеров классов, содержащихся во множестве [math]T'[/math];
  • [math]\mathtt{Size}[/math] — целочисленный массив, где [math]\mathtt{Size}[i][/math] хранит количество состояний из класса [math]i[/math], которые содержатся в [math]\mathtt{Inverse}[/math];
  • [math]\mathtt{Twin}[/math] — массив, хранящий в [math]\mathtt{Twin}[i][/math] номер нового класса, образовавшегося при разбиении класса [math]i[/math].
 //Добавим [math] F, Q \setminus F [/math] в [math] \mathtt{Queue} [/math], [math]\mathtt{InQueue}[/math]
 while [math]\mathtt{Queue} \ne \varnothing[/math]
   [math](C, a) \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{Size}[i] == 0[/math]
       insert [math]i[/math] in [math]\mathtt{Involved}[/math]
     [math]\mathtt{Size}[i]++[/math]
   for [math] i \in \mathtt{Involved}[/math]
   	if [math]\mathtt{Size}[i] \lt  \mathtt{Card}[i][/math]
   		[math]\mathtt{Size}[i] = -1[/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{Size}[i] == -1[/math]
       if [math]\mathtt{Twin}[i] == 0[/math]
         [math]\mathtt{Counter}++[/math]
         [math]\mathtt{Twin[i]} = \mathtt{Counter}[/math]
       move [math]r[/math] from [math]i[/math] to [math]\mathtt{Twin}[i][/math]
   for [math] j \in \mathtt{Involved}[/math]
     if [math] \mathtt{Twin}[j] \neq 0 [/math]
         for [math]c \in \Sigma[/math]
           [math]pushSetsToQueue(\mathtt{Queue}, j, \mathtt{Twin}[j], c)[/math]
     [math]\mathtt{Size}[j] = 0[/math]
     [math]\mathtt{Twin}[j] = 0[/math]
   remove [math]a[/math] from [math]\mathtt{InQueue}[C][/math]

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

Осталось только реализовать [math]pushSetsToQueue[/math].

 [math]pushSetsToQueue(\mathtt{Queue}, R_1, R_2, c)[/math]:
     [math]cnt1  \leftarrow  \mathtt{Card}[R_1][/math] 
     [math]cnt2  \leftarrow  \mathtt{Card}[R_2][/math] 
     if [math] \lnot ( \mathtt{InQueue}[R_1] [/math] contains [math] c) [/math] and [math] cnt1 \lt = cnt2 [/math]
       push [math](R_1, c)[/math] to [math]\mathtt{Queue}[/math]
       insert [math] c [/math] into [math]\mathtt{InQueue}[R_1][/math]
     else
       push [math](R_2, c)[/math] to [math]\mathtt{Queue}[/math]
       insert [math] c [/math] into [math]\mathtt{InQueue}[R_2][/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](C,a)[/math] добавленных в очередь [math]S[/math] не превосходит [math] 2 |\Sigma| |Q| [/math], так как на каждой итерации мы извлекаем одну пару из очереди.

По лемме(1) количество классов не превосходит [math]2 |Q| - 1[/math]. Пусть [math]C[/math] элемент текущего разбиения. Тогда количество пар [math](C,a), \ 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](C,a)[/math], где [math]p \in C[/math], которые мы удалим из очереди, не превосходит [math]\log_2(|Q|)[/math] для фиксированных [math]a[/math] и [math]p[/math].
Доказательство:
[math]\triangleright[/math]
Рассмотрим пару [math](C,a)[/math], где [math]p \in C[/math], которую мы удаляем из очереди. И пусть [math](C',a)[/math] следующая пара, где [math]p \in C'[/math] и которую мы удалим из очереди. Согласно нашему алгоритму класс [math]C'[/math] мог появиться в очереди только после операции [math]\mathtt{replace}[/math]. Но после первого же разбиения класса [math]C[/math] на подклассы мы добавим в очередь пару [math](C'', a)[/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](C, a)[/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{ClassInv}[/math], в [math]\mathtt{ClassInv}[C][a][/math] будем хранить множество состояний, из которых есть ребро по символу [math]a[/math] в состояние [math]C[/math] (аналогично [math]Inv[/math], только для классов).

[math]\mathtt{ClassInv}[C][a] = \{ s | \mathtt{Class}[s] == C [/math] and [math] \delta^{-1} (s, a) \neq \emptyset \}[/math]

[math]pushSetsToQueue[/math] реализуем так:

[math]pushSetsToQueue(\mathtt{Queue}, R_1, R_2, c)[/math]:

 [math]cnt1  \leftarrow  \mathtt{ClassInv}[R_1][c][/math] 
 [math]cnt2  \leftarrow  \mathtt{ClassInv}[R_2][c][/math] 
 if [math] \lnot ( \mathtt{InQueue}[R_1] [/math] contains [math] c) [/math] and [math] cnt1 \lt = cnt2 [/math]
   push [math](R_1, c)[/math] to [math]\mathtt{Queue}[/math]
   insert [math] c [/math] into [math]\mathtt{InQueue}[R_1][/math]
 else
   push [math](R_2, c)[/math] to [math]\mathtt{Queue}[/math]
   insert [math] c [/math] into [math]\mathtt{InQueue}[R_2][/math]

Циклы

for [math]q \in C[/math] and [math]r \in \mathtt{Inv}[q][a][/math] [math](...)[/math]

реализуются

for [math]q \in \mathtt{ClassInv}[C][a][/math] and [math]r \in \mathtt{Inv}[q][a][/math] [math](...)[/math]'

Тогда время работы внутреннего цикла можно будет оценить как [math]O(|\mathtt{ClassInv}[C][a]| + |\mathtt{Inverse}|)[/math]. А реализация [math]pushSetsToQueue[/math] выбирает множество, на котором [math]O(|\mathtt{ClassInv}[C][a]|)[/math] будет меньшим.

Кроме того, вместо хэш-таблиц для хранения множеств ([math]\mathtt{ClassInv}[/math], разбиение [math]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.
  • Оригинальная статья Хопкрофта