Пусть дан автомат, распознающий определенный язык. Требуется найти #перенаправление [[ Эквивалентность_состояний_ДКА | эквивалентный автомат]] с наименьшим количеством состояний. == Минимизация ДКА ==Если в ДКА существуют два [[ Эквивалентность_состояний_ДКА | эквивалентных состояния]], то при их объединении мы получим [[ Эквивалентность_состояний_ДКА | эквивалентный ДКА]], так как распознаваемый язык не изменится. Основная идея минимизации состоит в разбиении множества состояний на классы эквивалентности, полученные классы и будут состояниями минимизированного ДКА. == Простой алгоритм =={{Определение|definition =Класс <tex>C</tex> '''разбивает''' класс <tex>R</tex> по символу <tex>a</tex> на <tex>R_1</tex> и <tex>R_2</tex>, если # <tex>\forall r \in R_1 \,\,\, \delta(r, a) \in C</tex> # <tex>\forall r \in R_2 \,\,\, \delta(r, a) \notin C</tex> }} Если класс <tex>R</tex> может быть разбит по символу <tex>a</tex>, то он содержит хотя бы одну пару неэквивалентных состояний (так как существует строка которая их различает). Если класс нельзя разбить, то он состоит из эквивалентных состояний.Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно. Итеративно строим разбиение множества состояний следующим образом.# Первоначальное разбиение множества состояний {{---}} класс допускающих состояний <tex>F</tex> и класс недопускающих состояний <tex>Q \setminus F</tex>.# Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex>(F, c)</tex> и <tex>(Q \setminus F, c)</tex> помещаются в очередь.# Из очереди извлекается пара <tex>(C, a)</tex>, <tex>C</tex> далее именуется как сплиттер.# Все классы текущего разбиения разбиваются на 2 подкласса Хопкрофта (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>a</tex> переходят в сплиттер, а второй из всех оставшихся. # Те классы, которые разбились на два непустых подкласса, заменяются этими подклассами в разбиении, а также добавляются в очередь.# Пока очередь не пуста, выполняем п.3 – п.5. ===Псевдокод===<tex>Q</tex> {{---}} множество состояний ДКА.<tex>F</tex> {{---}} множество терминальных состояний.<tex>S</tex> {{---}} очередь пар <tex>(C, a)</tex>.<tex>P</tex> {{---}} разбиение множества состояний ДКА.<tex>R</tex> {{---}} класс состояний ДКА. <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> <tex>\mathtt{S} \leftarrow \varnothing </tex> '''for''' <tex>c \in \Sigma</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> '''while''' <tex> \mathtt{S} \ne \varnothing </tex> <tex>(C, a) \leftarrow</tex> '''pop'''(<tex>\mathtt{S}</tex>) '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</tex> <tex>R_1 = R \cap \delta^{-1} (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> '''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> '''insert''' <tex>(R_1, c)</tex> '''in''' <tex>\mathtt{S}</tex> '''insert''' <tex>(R_2, c)</tex> '''in''' <tex>\mathtt{S}</tex>Когда очередь <tex>S</tex> станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить. ===Время работы===Время работы алгоритма оценивается как <tex>сложность O(|\Sigma| \cdot n^2)</tex>, где <tex> log n </tex> {{---}} количество состояний ДКА, а <tex> \Sigma </tex>{{---}} алфавит. Это следует из того, что если пара <tex>(C, a)</tex> попала в очередь, и класс <tex>C</tex> использовался в качестве сплиттера, то при последующем разбиении этого класса в очередь добавляется два класса <tex>C_1</tex> и <tex>C_2</tex>, причем можно гарантировать лишь следующее уменьшение размера: <tex>|C| \ge |C_i| + 1</tex>. Каждое состояние изначально принадлежит лишь одному классу в очереди, поэтому каждый переход в автомате будет просмотрен не более, чем <tex>O(n)</tex> раз. Учитывая, что ребер всего <tex>O(|\Sigma| \cdot n)</tex>, получаем указанную оценку. == Алгоритм Хопкрофта== {{Лемма|statement = Класс <tex>R = R_1 \cup R_2</tex> и <tex>R_1 \cap R_2 = \varnothing</tex>, тогда разбиение всех классов (текущее разбиение) по символу <tex>a</tex> любыми двумя классами из <tex>R, R_1, R_2</tex> эквивалентно разбиению всех классов с помощью <tex>R, R_1, R_2</tex> по символу <tex>a</tex>.|proof = Разобьем все классы с помощью <tex>R </tex> и <tex> R_1</tex> по символу <tex>a</tex>, тогда для любого класса <tex>B</tex> из текущего разбиения выполняется:<tex>\forall r \in B \,\,\, \delta(r, a) \in R</tex> and <tex> \delta(r, a) \in R_1</tex> or :<tex>\forall r \in B \,\,\, \delta(r, a) \in R</tex> and <tex> \delta(r, a) \notin R_1</tex> or :<tex>\forall r \in B \,\,\, \delta(r, a) \notin R</tex> and <tex> \delta(r, a) \notin R_1</tex> А так как <tex>R = R_1 \cup R_2</tex> и <tex>R_1 \cap R_2 = \varnothing</tex> то выполняется:<tex>\forall r \in B \,\,\, \delta(r, a) \in R_2 </tex> or :<tex> \forall r \in B \,\,\, \delta(r, a) \notin R_2</tex> Из этого следует, что разбиение всех классов с помощью <tex>R_2</tex> никак не повлияет на текущее разбиение. <br/>Аналогично доказывается и для разбиения с помощью <tex>R </tex> и <tex> R_2</tex> по символу <tex>a</tex>. <br/>Разобьем все классы с помощью <tex>R_1</tex> и <tex> R_2</tex> по символу <tex>a</tex>, тогда для любого класса <tex>B</tex> из текущего разбиения выполняется:<tex>\forall r \in B \,\,\, \delta(r, a) \in R_1</tex> and <tex> \delta(r, a) \notin R_2</tex> or :<tex>\forall r \in B \,\,\, \delta(r, a) \notin R_1</tex> and <tex> \delta(r, a) \in R_2</tex> or :<tex>\forall r \in B \,\,\, \delta(r, a) \notin R_1</tex> and <tex> \delta(r, a) \notin R_2</tex> А так как <tex>R = R_1 \cup R_2</tex> и <tex>R_1 \cap R_2 = \varnothing</tex> то выполняется:<tex>\forall r \in B \,\,\, \delta(r, a) \in R </tex> or :<tex> \forall r \in B \,\,\, \delta(r, a) \notin R</tex> Из этого следует, что разбиение всех классов с помощью <tex>R</tex> никак не повлияет на текущее разбиение.}} Алгоритм Хопкрофта отличается от простого тем, что иначе добавляет классы в очередь.Если класс <tex>R</tex> уже есть в очереди, то согласно лемме можно просто заменить его на <tex>R_1</tex> и <tex>R_2</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> \forall r \in B \,\,\, \delta(r, a) \notin R</tex> то в очередь можно добавить только меньшее из <tex>R_1</tex> и <tex>R_2</tex>. ===Реализация===<tex>Q</tex> {{---}} множество состояний ДКА.<tex>F</tex> {{---}} множество терминальных состояний.<tex>S</tex> {{---}} очередь из пар <tex>(C, a)</tex>.<tex>P</tex> {{---}} разбиение множества состояний ДКА.<tex>R</tex> {{---}} класс состояний ДКА. <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> <tex>\mathtt{S} \leftarrow \varnothing </tex> '''for''' <tex>c \in \Sigma</tex> '''insert''' <tex> (\mathtt{min} (\mathtt{F, Q} \setminus \mathtt{F}), c)</tex> '''in''' <tex>\mathtt{S}</tex> '''while''' <tex>S \ne \varnothing</tex> <tex>(C, a) \leftarrow</tex> '''pop'''(<tex>\mathtt{S}</tex>) <tex>T = \{R \ | \ R \in \mathtt{P}, \ R</tex> '''split by''' <tex>(C, a) \}</tex> '''for each''' <tex>R</tex> '''in''' <tex>T</tex> <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</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> '''if''' <tex>(R, c)</tex> '''in''' <tex>\mathtt{S}</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>. Модифицируем наш алгоритм: для каждой очередной пары <tex> (C, a) </tex> будем находить <tex> T' </tex>, и с каждым классом состояний из <tex> T' </tex> будем производить те же действия, что и раньше. <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> <tex>\mathtt{S} \leftarrow \varnothing </tex> '''for''' <tex>c \in \Sigma</tex> '''insert''' <tex>(\mathtt{min} (\mathtt{F, Q} \setminus \mathtt{F}), c)</tex> '''in''' <tex>\mathtt{S}</tex> '''while''' <tex>\mathtt{S} \ne \varnothing</tex> <tex>(C, a) \leftarrow</tex> '''pop'''(<tex>\mathtt{S}</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> '''for each''' <tex>R</tex> '''in''' <tex>T'</tex> '''if''' <tex>R</tex> '''split by''' <tex>(C, a)</tex> <tex> R_1, R_2 \leftarrow </tex> '''split'''(<tex>R, C, a</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> '''if''' <tex>(R, c)</tex> '''in''' <tex>\mathtt{S}</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(|Inverse|) </tex> для текущей пары <tex> (C,a)</tex>. Покажем, как достичь этой оценки. Разбиение <tex> P </tex> можно поддерживать четырьмя массивами:*<tex>Class[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex>;*<tex>Part[i]</tex> {{---}} указатель на голову [[Список#Двусвязный список|двусвязного списка]], содержащего состояния, принадлежащие классу <tex> i </tex>;*<tex>Card[i]</tex> {{---}} количество состояний в классе <tex>i</tex>;*<tex>Place[r]</tex> {{---}} указатель на состояние <tex>r</tex> в списке <tex>Part[Class[r]]</tex>. Так как мы храним указатель, где находится состояние в двусвязном списке, то операцию перемещения состояния из одного класса в другой можно выполнить за <tex>O(1)</tex>. Чтобы эффективно находить множество <tex>Inverse</tex>, построим массив <tex>Inv</tex>, который для состояния <tex>r</tex> и символа <tex>a</tex> в <tex>Inv[r][a]</tex> хранит множество состояний, из которых существует переход в <tex>r</tex> по символу <tex>a</tex>. Так как наш алгоритм не меняет изначальный автомат, то массив <tex>Inv</tex> можно построить перед началом основной части алгоритма, что займет <tex>O(|\Sigma| |Q|)</tex> времени. Теперь научимся за <tex>O(|Inverse|)</tex> обрабатывать множество <tex>T'</tex> и разбивать классы. Для этого нам понадобится следующая структура:*<tex>Counter</tex> {{---}} количество классов;*<tex>Involved</tex> {{---}} список из номеров классов, содержащихся во множестве <tex>T'</tex>;*<tex>Size</tex> {{---}} целочисленный массив, где <tex>Size[i]</tex> хранит количество состояний из класса <tex>i</tex>, которые содержатся в <tex>Inverse</tex>;*<tex>Twin</tex> {{---}} массив, хранящий в <tex>Twin[i]</tex> номер нового класса, образовавшегося при разбиении класса <tex>i</tex>. Сам же алгоритм обработки <tex>T'</tex> будет выглядеть так: <tex>\mathtt{Involved} \leftarrow \varnothing</tex> '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex> <tex>i = \mathtt{Class}[q]</tex> '''if''' <tex>\mathtt{Size}[i] == 0</tex> '''insert''' <tex>i</tex> '''in''' <tex>\mathtt{Involved}</tex> <tex>\mathtt{Size}[i]++</tex> '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex> <tex>i = \mathtt{Class}[q]</tex> '''if''' <tex>\mathtt{Size}[i] < \mathtt{Card}[i]</tex> '''if''' <tex>\mathtt{Twin}[i] == 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> <tex>\mathtt{Size}[j] = 0</tex> <tex>\mathtt{Twin}[j] = 0</tex> Для быстрой проверки, находится ли пара <tex>(C,a)</tex> в очереди <tex>S</tex>, будем использовать массив <tex>InQueue</tex> размера <tex>|Q| \times |\Sigma|</tex>, где <tex>InQueue[C][a] = true</tex>, если пара <tex>(C,a)</tex> содержится в очереди. Так как при разбиении очередного класса <tex>R</tex> на подклассы <tex>R_1</tex> и <tex>R_2</tex> мы в действительности создаем лишь один новый класс, то замена класса <tex>R</tex> в очереди на подклассы, образовавшиеся при разбиении, сводится лишь к взаимодействию с массивом <tex>InQueue</tex>. В результате каждая операция с очередью требует <tex>O(1)</tex> времени. ===Время работы=== {{Лемма|about = 1|id = Лемма1|statement =Количество классов, созданных во время выполнения алгоритма, не превышает <tex>2 |Q| - 1</tex>.|proof =Представим дерево, которое соответствует операциям разделения классов на подклассы. Корнем этого дерева является все множество состояний <tex>Q</tex>. Листьями являются классы эквивалентности, оставшиеся после работы алгоритма. Так как дерево бинарное {{---}} каждый класс может породить лишь два новых, а количество листьев не может быть больше <tex>|Q|</tex>, то количество узлов этого дерева не может быть больше <tex>2 |Q| - 1</tex>, что доказывает утверждение леммы.}} {{Лемма|about = 2|id = Лемма2|statement = Количество итераций цикла <tex>while</tex> не превышает <tex> 2 |\Sigma| |Q| </tex>.|proof =Для доказательства этого утверждения достаточно показать, что количество пар <tex>(C,a)</tex> добавленных в очередь <tex>S</tex> не превосходит <tex> 2 |\Sigma| |Q| </tex>, так как на каждой итерации мы извлекаем одну пару из очереди. По [[#Лемма1 | лемме(1)]] количество классов не превосходит <tex>2 |Q| - 1</tex>. Пусть <tex>C</tex> элемент текущего разбиения. Тогда количество пар <tex>(C,a), \ a \in \Sigma</tex> не может быть больше <tex>|\Sigma|</tex>. Отсюда следует, что всего различных пар, которые можно добавить в очередь, не превосходит <tex> 2 |\Sigma| |Q| </tex>.}} {{Лемма|about = 3|id = Лемма3|statement = Пусть <tex>a \in \Sigma</tex> и <tex>p \in Q</tex>. Тогда количество пар <tex>(C,a)</tex>, где <tex>p \in C</tex>, которые мы удалим из очереди, не превосходит <tex>\log_2(|Q|)</tex> для фиксированных <tex>a</tex> и <tex>p</tex>.|proof =Рассмотрим пару <tex>(C,a)</tex>, где <tex>p \in C</tex>, которую мы удаляем из очереди. И пусть <tex>(C',a)</tex> следующая пара, где <tex>p \in C'</tex> и которую мы удалим из очереди. Согласно нашему алгоритму класс <tex>C'</tex> мог появиться в очереди только после операции <tex>replace</tex>. Но после первого же разбиения класса <tex>C</tex> на подклассы мы добавим в очередь пару <tex>(C'', a)</tex>, где <tex>C''</tex> меньший из образовавшихся подклассов, то есть <tex>|C''| \leqslant |C| \ / \ 2</tex>. Так же заметим, что <tex>C' \subseteq C''</tex>, а следовательно <tex>|C'| \leqslant |C| \ / \ 2</tex>. Но тогда таких пар не может быть больше, чем <tex>\log_2(|Q|)</tex>. }} {{Лемма|about = 4|id = Лемма4|statement = <tex>\sum |Inverse|</tex> по всем итерациям цикла <tex>while</tex> не превосходит <tex>|\Sigma| |Q| \log_2(|Q|)</tex>.|proof =Пусть <tex>x, y \in Q</tex>, <tex>a \in \Sigma</tex> и <tex> \delta(x, a) = y</tex>. Зафиксируем эту тройку. Заметим, что количество раз, которое <tex>x</tex> встречается в <tex>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> мы получим утверждение леммы.}} {{Теорема|statement =Время работы алгоритма Хопкрофта равно <tex>O(|\Sigma| |Q| \log(|Q|)</tex>.|proof =Оценим, сколько времени занимает каждая часть алгоритма: *Построение массива <tex>Inv</tex> занимает <tex>O(|\Sigma| |Q|)</tex> времени. *По [[#Лемма2 | второй лемме]] количество итераций цикла <tex>while</tex> не превосходит <tex>O(|\Sigma| |Q|)</tex>. *Операции с множеством <tex>T'</tex> и разбиение классов на подклассы требуют <tex>O(\sum(|Inverse|))</tex> времени. Но по [[#Лемма4 | лемме(4)]] <tex>\sum(|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>replace</tex> равно <tex>O(|\Sigma| |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>.}} == Литература ==* ''Хопкрофт Д., Мотвани Р., Ульман Д.'' Введение в теорию автоматов, языков и вычислений, 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. [[Категория: Теория формальных языков]][[Категория: Автоматы и регулярные языки]]