Минимизация ДКА, алгоритм Хопкрофта (сложность O(n log n)) — различия между версиями
(Тикет 2-17) |
|||
Строка 16: | Строка 16: | ||
Итеративно строим разбиение множества состояний следующим образом. | Итеративно строим разбиение множества состояний следующим образом. | ||
# Первоначальное разбиение множества состояний {{---}} класс допускающих состояний <tex>F</tex> и класс недопускающих состояний (<tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex>). | # Первоначальное разбиение множества состояний {{---}} класс допускающих состояний <tex>F</tex> и класс недопускающих состояний (<tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex>). | ||
− | # Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <tex> | + | # Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <<tex>F,\ c</tex>> и <<tex>Q \setminus F, c</tex>> помещаются в очередь. |
− | # Из очереди извлекается пара <tex> | + | # Из очереди извлекается пара <<tex>C,\ a</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 | + | # Если <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> | + | ## Перебираются символы алфавита <tex>c \in \Sigma</tex>, все пары <<tex>R_1, c</tex>> и <<tex>R_2, c</tex>> помещаются в очередь. |
# Пока очередь не пуста, выполняем п.3 – п.5. | # Пока очередь не пуста, выполняем п.3 – п.5. | ||
===Псевдокод=== | ===Псевдокод=== | ||
− | <tex>Q</tex> {{---}} множество состояний ДКА. | + | *<tex>Q</tex> {{---}} множество состояний ДКА. |
− | <tex>F</tex> {{---}} множество терминальных состояний. | + | *<tex>F</tex> {{---}} множество терминальных состояний. |
− | <tex>S</tex> {{---}} очередь пар <tex> | + | *<tex>\delta</tex> {{---}} функция перехода (<tex>\delta (r,\ a)</tex> - состояние, в которое можно совершить переход из <tex>r</tex> по символу <tex>a</tex>) |
− | <tex>P</tex> {{---}} разбиение множества состояний ДКА. | + | *<tex>S</tex> {{---}} очередь пар <<tex>C,\ a</tex>>. |
− | <tex>R</tex> {{---}} класс состояний ДКА. | + | *<tex>P</tex> {{---}} разбиение множества состояний ДКА. |
+ | *<tex>R</tex> {{---}} класс состояний ДКА. | ||
+ | |||
+ | <tex>\mathtt{findEquivalenceClasses}(\mathtt{Q},\ \mathtt{F},\ \delta)</tex> | ||
+ | <tex>\mathtt{P} \leftarrow \{ \mathtt{F,\ Q} \setminus \mathtt{F} \}</tex> | ||
+ | <tex>\mathtt{S} \leftarrow \varnothing </tex> | ||
+ | '''for''' <tex>c \in \Sigma</tex> | ||
+ | '''push''' <<tex>F,\ c</tex>>, <<tex>Q \setminus F,\ c</tex>> '''into''' <tex> \mathtt{S}</tex> | ||
+ | '''while''' <tex>\mathtt{S} \ne \varnothing</tex> | ||
+ | <<tex>C,\ a</tex>> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex> | ||
+ | '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</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> | ||
+ | '''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> | ||
+ | '''return''' <tex>\mathtt{P}</tex> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
Когда очередь <tex>S</tex> станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить. | Когда очередь <tex>S</tex> станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить. | ||
===Время работы=== | ===Время работы=== | ||
− | Время работы алгоритма оценивается как <tex>O(|\Sigma| \cdot n^2)</tex>, где <tex> n </tex> {{---}} количество состояний ДКА, а <tex> \Sigma </tex>{{---}} алфавит. Это следует из того, что если пара <tex> | + | Время работы алгоритма оценивается как <tex>O(|\Sigma| \cdot n^2)</tex>, где <tex> 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>, получаем указанную оценку. |
== Алгоритм Хопкрофта== | == Алгоритм Хопкрофта== | ||
Строка 57: | Строка 60: | ||
|proof = | |proof = | ||
Разобьем все классы с помощью <tex>R </tex> и <tex> R_1</tex> по символу <tex>a</tex>, тогда для любого класса <tex>B</tex> из текущего разбиения выполняется | Разобьем все классы с помощью <tex>R </tex> и <tex> R_1</tex> по символу <tex>a</tex>, тогда для любого класса <tex>B</tex> из текущего разбиения выполняется | ||
− | :<tex>\forall r \in B \,\,\, \delta(r, a) \in R | + | :<tex>\forall r \in B \,\,\, \delta(r, a) \in R \ \land \ \delta(r, a) \in R_1</tex> |
− | :<tex>\forall r \in B \,\,\, \delta(r, a) \in R | + | :<tex> \ \lor \ \forall r \in B \,\,\, \delta(r, a) \in R \ \land \ \delta(r, a) \notin R_1</tex> |
− | :<tex>\forall r \in B \,\,\, \delta(r, a) \notin R | + | :<tex> \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R \ \land \ \delta(r, a) \notin R_1</tex> |
А так как <tex>R = R_1 \cup R_2</tex> и <tex>R_1 \cap R_2 = \varnothing</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> | + | :<tex>\forall r \in B \,\,\, \delta(r, a) \in R_2 </tex> |
− | :<tex> \forall r \in B \,\,\, \delta(r, a) \notin R_2</tex> | + | :<tex> \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R_2</tex> |
Из этого следует, что разбиение всех классов с помощью <tex>R_2</tex> никак не повлияет на текущее разбиение. <br/> | Из этого следует, что разбиение всех классов с помощью <tex>R_2</tex> никак не повлияет на текущее разбиение. <br/> | ||
Аналогично доказывается и для разбиения с помощью <tex>R </tex> и <tex> R_2</tex> по символу <tex>a</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>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>\forall r \in B \,\,\, \delta(r, a) \in R_1 \ \land \ \delta(r, a) \notin R_2</tex> |
− | :<tex>\forall r \in B \,\,\, \delta(r, a) \notin R_1 | + | :<tex> \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R_1 \ \land \ \delta(r, a) \in R_2</tex> |
− | :<tex>\forall r \in B \,\,\, \delta(r, a) \notin R_1 | + | :<tex> \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R_1 \ \land \ \delta(r, a) \notin R_2</tex> |
А так как <tex>R = R_1 \cup R_2</tex> и <tex>R_1 \cap R_2 = \varnothing</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> | + | :<tex>\forall r \in B \,\,\, \delta(r, a) \in R </tex> |
− | :<tex> \forall r \in B \,\,\, \delta(r, a) \notin R</tex> | + | :<tex> \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R</tex> |
Из этого следует, что разбиение всех классов с помощью <tex>R</tex> никак не повлияет на текущее разбиение. | Из этого следует, что разбиение всех классов с помощью <tex>R</tex> никак не повлияет на текущее разбиение. | ||
}} | }} | ||
Строка 78: | Строка 81: | ||
После замены класса <tex>R</tex> в разбиении <tex>P</tex> на его подклассы <tex>R_1</tex> и <tex>R_2</tex>, как и раньше перебираем символы алфавита <tex>c \in \Sigma</tex>. | После замены класса <tex>R</tex> в разбиении <tex>P</tex> на его подклассы <tex>R_1</tex> и <tex>R_2</tex>, как и раньше перебираем символы алфавита <tex>c \in \Sigma</tex>. | ||
− | Если пара <tex> | + | Если пара <<tex>R,\ c</tex>> уже есть в очереди, то согласно лемме можно просто заменить её на пары <<tex>R_1, c</tex>> и <<tex>R_2, c</tex>>. |
− | Если пары <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>\mathtt{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> | + | *<tex>\delta</tex> {{---}} функция перехода (<tex>\delta (r,\ a)</tex> - состояние, в которое можно совершить переход из <tex>r</tex> по символу <tex>a</tex>) |
− | <tex>P</tex> {{---}} разбиение множества состояний ДКА. | + | *<tex>S</tex> {{---}} очередь пар <<tex>C,\ a</tex>>. |
− | <tex>R</tex> {{---}} класс состояний ДКА. | + | *<tex>P</tex> {{---}} разбиение множества состояний ДКА. |
+ | *<tex>R</tex> {{---}} класс состояний ДКА. | ||
− | <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> | + | <tex>\mathtt{findEquivalenceClasses}(\mathtt{Q},\ \mathtt{F},\ \delta)</tex> |
− | + | <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> | |
− | + | <tex>\mathtt{S} \leftarrow \varnothing </tex> | |
− | + | '''for''' <tex>c \in \Sigma</tex> | |
− | + | '''push''' <<tex>F,\ c</tex>>, <<tex>Q \setminus F,\ c</tex>> '''into''' <tex> \mathtt{S}</tex> | |
− | + | '''while''' <tex>\mathtt{S} \ne \varnothing</tex> | |
− | + | <<tex>C,\ a</tex>> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{S}</tex> | |
− | + | '''for''' <tex>R</tex> '''in''' <tex>\mathtt{P}</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> | |
− | + | '''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> | |
+ | <tex>\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2,\ c)</tex> | ||
+ | '''return''' <tex>\mathtt{P}</tex> | ||
− | Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за T' (его нужно будет эффективно находить для каждой пары <tex> | + | Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за T' (его нужно будет эффективно находить для каждой пары <<tex>C,\ a</tex>>). |
− | <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> | + | <tex>\mathtt{findEquivalenceClasses}(\mathtt{Q},\ \mathtt{F},\ \delta)</tex> |
− | + | <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> | |
− | + | <tex>\mathtt{S} \leftarrow \varnothing </tex> | |
− | + | '''for''' <tex>c \in \Sigma</tex> | |
− | + | '''push''' <<tex>F,\ c</tex>>, <<tex>Q \setminus F,\ c</tex>> '''into''' <tex> \mathtt{S}</tex> | |
− | + | '''while''' <tex>\mathtt{S} \ne \varnothing</tex> | |
− | + | <<tex>C,\ a</tex>> <tex>\leftarrow</tex> '''pop''' '''from''' <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''' <tex>R</tex> '''in''' <tex>T'</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> | |
− | + | '''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> | |
+ | <tex>\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2,\ c)</tex> | ||
+ | '''return''' <tex>\mathtt{P}</tex> | ||
− | Каждая итерация цикла <tex> \mathrm{while} </tex> может быть выполнена за <tex> O(|Q| + |\mathtt{Inverse}|) </tex> для текущей пары <tex> | + | Каждая итерация цикла <tex> \mathrm{while} </tex> может быть выполнена за <tex> O(|Q| + |\mathtt{Inverse}|) </tex> для текущей пары <<tex>C,\ a</tex>>. Покажем, как можно достичь этой оценки. |
− | Классы разбиения <tex>P</tex> будем поддерживать с помощью множеств на хэш-таблицах (само же разбиение - обычный вектор, индекс - номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за O(1)). | + | Классы разбиения <tex>P</tex> будем поддерживать с помощью множеств на [[Хеш-таблица | хэш-таблицах]] (само же разбиение - обычный вектор, индекс - номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за O(1)). |
*<tex>\mathtt{Class}[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex> | *<tex>\mathtt{Class}[r]</tex> {{---}} номер класса, которому принадлежит состояние <tex>r</tex> | ||
*<tex>\mathtt{Card}[i]</tex> {{---}} размер класса <tex>i</tex> | *<tex>\mathtt{Card}[i]</tex> {{---}} размер класса <tex>i</tex> | ||
− | *<tex>\mathtt{Queue}</tex> {{---}} очередь пар <tex> | + | *<tex>\mathtt{Queue}</tex> {{---}} очередь пар <<tex>C,\ a</tex>>, где <tex>C</tex> {{---}} номер класса (сплиттера) |
− | *<tex>\mathtt{InQueue} | + | *<tex>\mathtt{InQueue}</tex> {{---}} двумерный массив булеанов, <tex>\mathtt{InQueue}[C][a] == true</tex>, если <<tex>C,\ a</tex>> находится в очереди <tex>\mathtt{Queue}</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> (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма) | ||
Строка 138: | Строка 146: | ||
*<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}(\mathtt{Q},\ \mathtt{F},\ \delta)</tex> |
− | + | <tex>\mathtt{P} \leftarrow \{ \mathtt{F, Q} \setminus \mathtt{F} \}</tex> | |
− | + | '''for''' <tex>c \in \Sigma</tex> | |
− | + | '''push''' <<tex>F,\ c</tex>>, <<tex>Q \setminus F,\ c</tex>> '''into''' <tex> \mathtt{Queue}</tex> | |
− | + | <tex>\mathtt{InQueue}[F][c] \ \leftarrow \ true</tex> | |
− | + | <tex>\mathtt{InQueue}[Q \setminus F][c] \ \leftarrow \ true</tex> | |
− | + | '''while''' <tex>\mathtt{Queue} \ne \varnothing</tex> | |
− | + | <<tex>C,\ a</tex>> <tex>\leftarrow</tex> '''pop''' '''from''' <tex>\mathtt{Queue}</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}[r]</tex> | |
− | + | '''if''' <tex>\mathtt{Size}[i] == 0</tex> | |
− | + | '''insert''' <tex>i</tex> '''in''' <tex>\mathtt{Involved}</tex> | |
− | + | <tex>\mathtt{Size}[i]++</tex> | |
− | + | '''for''' <tex> i \in \mathtt{Involved}</tex> | |
− | + | '''if''' <tex>\mathtt{Size}[i] < \mathtt{Card}[i]</tex> | |
− | + | <tex>\mathtt{Size}[i] = -1</tex> //Пометим сразу, т.к. в следующем цикле классы уже будут менятся | |
− | + | '''for''' <tex>q \in C</tex> '''and''' <tex>r \in \mathtt{Inv}[q][a]</tex> | |
− | + | <tex>i = \mathtt{Class}[r]</tex> | |
− | + | '''if''' <tex>\mathtt{Size}[i] == -1</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> '''in''' <tex>\mathtt{P}</tex> | |
− | + | '''for''' <tex> j \in \mathtt{Involved}</tex> | |
− | + | '''if''' <tex> \mathtt{Twin}[j] \neq 0 </tex> | |
+ | '''for''' <tex>c \in \Sigma</tex> | ||
+ | <tex>\mathtt{pushSetsToQueue}(\mathtt{Queue},\ j,\ \mathtt{Twin}[j],\ c)</tex> | ||
+ | <tex>\mathtt{Size}[j] = 0</tex> | ||
+ | <tex>\mathtt{Twin}[j] = 0</tex> | ||
+ | <tex>\mathtt{InQueue}[C][a] \ \leftarrow \ false</tex> | ||
+ | '''return''' <tex>\mathtt{P}</tex> | ||
Стоит отметить, что массивы <tex>\mathtt{Size}, \mathtt{Twin}</tex> аллоцируются ровно один раз при инициализации алгоритма. | Стоит отметить, что массивы <tex>\mathtt{Size}, \mathtt{Twin}</tex> аллоцируются ровно один раз при инициализации алгоритма. | ||
− | Осталось только реализовать <tex>pushSetsToQueue</tex>. | + | Осталось только реализовать <tex>\mathtt{pushSetsToQueue}</tex>. |
− | <tex>pushSetsToQueue(\mathtt{Queue}, R_1, R_2, c)</tex>: | + | <tex>\mathtt{pushSetsToQueue}(\mathtt{Queue},\ R_1,\ R_2,\ c)</tex>: |
<tex>cnt1 \leftarrow \mathtt{Card}[R_1]</tex> | <tex>cnt1 \leftarrow \mathtt{Card}[R_1]</tex> | ||
<tex>cnt2 \leftarrow \mathtt{Card}[R_2]</tex> | <tex>cnt2 \leftarrow \mathtt{Card}[R_2]</tex> | ||
− | '''if''' <tex> | + | '''if''' <tex> \mathtt{InQueue}[R_1][c] == false </tex> '''and''' <tex> cnt1 \leqslant cnt2 </tex> |
− | '''push''' <tex> | + | '''push''' <<tex>R_1, c</tex>> '''to''' <tex>\mathtt{Queue}</tex> |
− | + | <tex>\mathtt{InQueue}[R_1][c] \ \leftarrow \ true</tex> | |
'''else''' | '''else''' | ||
− | '''push''' <tex> | + | '''push''' <<tex>R_2, c</tex>> '''to''' <tex>\mathtt{Queue}</tex> |
− | + | <tex>\mathtt{InQueue}[R_2][c] \ \leftarrow \ true</tex> | |
===Время работы=== | ===Время работы=== | ||
Строка 195: | Строка 209: | ||
Количество итераций цикла <tex>\mathrm{while}</tex> не превышает <tex> 2 |\Sigma| |Q| </tex>. | Количество итераций цикла <tex>\mathrm{while}</tex> не превышает <tex> 2 |\Sigma| |Q| </tex>. | ||
|proof = | |proof = | ||
− | Для доказательства этого утверждения достаточно показать, что количество пар <tex> | + | Для доказательства этого утверждения достаточно показать, что количество пар <<tex>C,\ a</tex>> добавленных в очередь <tex>S</tex> не превосходит <tex> 2 |\Sigma| |Q| </tex>, так как на каждой итерации мы извлекаем одну пару из очереди. |
− | По [[#Лемма1 | лемме(1)]] количество классов не превосходит <tex>2 |Q| - 1</tex>. Пусть <tex>C</tex> элемент текущего разбиения. Тогда количество пар <tex> | + | По [[#Лемма1 | лемме(1)]] количество классов не превосходит <tex>2 |Q| - 1</tex>. Пусть <tex>C</tex> элемент текущего разбиения. Тогда количество пар <<tex>C,\ a</tex>>, <tex>\ a \in \Sigma</tex> не может быть больше <tex>|\Sigma|</tex>. Отсюда следует, что всего различных пар, которые можно добавить в очередь, не превосходит <tex> 2 |\Sigma| |Q| </tex>. |
}} | }} | ||
Строка 204: | Строка 218: | ||
|id = Лемма3 | |id = Лемма3 | ||
|statement = | |statement = | ||
− | Пусть <tex>a \in \Sigma</tex> и <tex>p \in Q</tex>. Тогда количество пар <tex> | + | Пусть <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 = | |proof = | ||
− | Рассмотрим пару <tex> | + | Рассмотрим пару <<tex>C,\ a</tex>>, где <tex>p \in C</tex>, которую мы удаляем из очереди. И пусть <<tex>C',a</tex>> следующая пара, где <tex>p \in C'</tex> и которую мы удалим из очереди. Согласно нашему алгоритму класс <tex>C'</tex> мог появиться в очереди только после операции <tex>\mathtt{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>. |
}} | }} | ||
Строка 215: | Строка 229: | ||
<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> | + | Пусть <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> мы получим утверждение леммы. |
}} | }} | ||
Строка 235: | Строка 249: | ||
}} | }} | ||
− | == Сравнение с алгоритмом из оригинальной статьи | + | == Сравнение с алгоритмом из оригинальной статьи Хопкрофта == |
− | В [http://i.stanford.edu/pub/cstr/reports/cs/tr/71/190/CS-TR-71-190.pdf | + | В оригинальной статье <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 | + | <tex>\mathtt{ClassInv}[C][a] = \{ s\ |\ \mathtt{Class}[s] == C \ \land \ \delta^{-1} (s, a) \neq \emptyset \}</tex> |
− | <tex>pushSetsToQueue</tex> реализуем так: | + | <tex>\mathtt{pushSetsToQueue}</tex> реализуем так: |
− | <tex>pushSetsToQueue(\mathtt{Queue}, R_1, R_2, c)</tex> | + | <tex>\mathtt{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> \mathtt{InQueue}[R_1][c] == false </tex> '''and''' <tex> cnt1 \leqslant 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> | |
− | + | (...) | |
− | реализуются | + | реализуются так: |
− | + | '''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>pushSetsToQueue</tex> выбирает множество, на котором <tex>O(|\mathtt{ClassInv}[C][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>) можно использовать комбинацию из двусвязного списка и вектора (добавление/удаление через список, поиск через вектор). Что и используется в оригинальной статье. | + | Кроме того, вместо [[Хеш-таблица | хэш-таблиц]] для хранения множеств (<tex>\mathtt{ClassInv}</tex>, разбиение <tex>P</tex>) можно использовать комбинацию из двусвязного списка и вектора (добавление/удаление через список, поиск через вектор). Что и используется в оригинальной статье. |
− | == | + | == Источники информации == |
* ''Хопкрофт Д., Мотвани Р., Ульман Д.'' Введение в теорию автоматов, языков и вычислений, 2-е изд. : Пер. с англ. — М.: Издательский дом «Вильямс», 2002. — С. 177 — ISBN 5-8459-0261-4 (рус.) | * ''Хопкрофт Д., Мотвани Р., Ульман Д.'' Введение в теорию автоматов, языков и вычислений, 2-е изд. : Пер. с англ. — М.: Издательский дом «Вильямс», 2002. — С. 177 — ISBN 5-8459-0261-4 (рус.) | ||
* ''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 | + | * [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] |
+ | |||
+ | == Примечания == | ||
+ | |||
+ | <references/> | ||
[[Категория: Теория формальных языков]] | [[Категория: Теория формальных языков]] | ||
[[Категория: Автоматы и регулярные языки]] | [[Категория: Автоматы и регулярные языки]] |
Версия 12:42, 11 января 2015
Пусть дан автомат, распознающий определенный язык. Требуется найти эквивалентный автомат с наименьшим количеством состояний.
Содержание
Минимизация ДКА
Если в ДКА существуют два эквивалентных состояния, то при их объединении мы получим эквивалентный ДКА, так как распознаваемый язык не изменится. Основная идея минимизации состоит в разбиении множества состояний на классы эквивалентности, полученные классы и будут состояниями минимизированного ДКА.
Простой алгоритм
Определение: |
Класс | разбивает класс по символу на и , если
Если класс
может быть разбит по символу , то он содержит хотя бы одну пару неэквивалентных состояний (так как существует строка которая их различает). Если класс нельзя разбить, то он состоит из эквивалентных состояний. Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно.Итеративно строим разбиение множества состояний следующим образом.
- Первоначальное разбиение множества состояний — класс допускающих состояний и класс недопускающих состояний ( ).
- Перебираются символы алфавита , все пары < > и < > помещаются в очередь.
- Из очереди извлекается пара < >, далее именуется как сплиттер.
- Каждый класс текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу переходят в сплиттер ( ), а второй из всех оставшихся ( ).
- Если
- В разбиении класс заменяется на свои подклассы и .
- Перебираются символы алфавита , все пары < > и < > помещаются в очередь.
разбился на два непустых подкласса (т.е. ).
- Пока очередь не пуста, выполняем п.3 – п.5.
Псевдокод
- — множество состояний ДКА.
- — множество терминальных состояний.
- — функция перехода ( - состояние, в которое можно совершить переход из по символу )
- — очередь пар < >.
- — разбиение множества состояний ДКА.
- — класс состояний ДКА.
for push < >, < > into while < > pop from for in if and replace in with and for insert < > in insert < > in return
Когда очередь
станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить.Время работы
Время работы алгоритма оценивается как
, где — количество состояний ДКА, а — алфавит. Это следует из того, что если пара < > попала в очередь, и класс использовался в качестве сплиттера, то при последующем разбиении этого класса в очередь добавляется два класса и , причем можно гарантировать лишь следующее уменьшение размера: . Каждое состояние изначально принадлежит лишь одному классу в очереди, поэтому каждый переход в автомате будет просмотрен не более, чем раз. Учитывая, что ребер всего , получаем указанную оценку.Алгоритм Хопкрофта
Рассмотрим алгоритм, позволяющий решить задачу быстрее, чем за
.Лемма: |
Класс и , тогда разбиение всех классов (текущее разбиение) по символу любыми двумя классами из эквивалентно разбиению всех классов с помощью по символу . |
Доказательство: |
Разобьем все классы с помощью и по символу , тогда для любого класса из текущего разбиения выполняетсяА так как и то выполняетсяИз этого следует, что разбиение всех классов с помощью А так как и то выполняется |
Алгоритм Хопкрофта отличается от простого тем, что иначе добавляет пары в очередь. После замены класса
в разбиении на его подклассы и , как и раньше перебираем символы алфавита .Если пара <
> уже есть в очереди, то согласно лемме можно просто заменить её на пары < > и < >.Если пары <
> нет в очереди, то достаточно добавить любую из пар < > и < >. Это следует из следующих соображений: может быть в разбиении только если в очередь были положены пары < > для , а поскольку в очереди пары < > нет, то мы её уже успели рассмотреть, следовательно классы из разбиения уже были разбиты по < >.Реализация
— функция, которая добавляет одну из пар < >, < > в очередь S.
- — множество состояний ДКА.
- — множество терминальных состояний.
- — функция перехода ( - состояние, в которое можно совершить переход из по символу )
- — очередь пар < >.
- — разбиение множества состояний ДКА.
- — класс состояний ДКА.
for push < >, < > into while < > pop from for in if and replace in with and for return
Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за T' (его нужно будет эффективно находить для каждой пары <
>).for push < >, < > into while < > pop from for in if and replace in with and for return
Каждая итерация цикла может быть выполнена за для текущей пары < >. Покажем, как можно достичь этой оценки.
Классы разбиения хэш-таблицах (само же разбиение - обычный вектор, индекс - номер класса). Это позволит нам эффективно переносить состояния из одного класса в другой (за O(1)).
будем поддерживать с помощью множеств на- — номер класса, которому принадлежит состояние
- — размер класса
- — очередь пар < >, где — номер класса (сплиттера)
- — двумерный массив булеанов, , если < > находится в очереди
- — массив состояний, из которых есть ребра по символу в состояние (мы не меняем исходный автомат, потому может быть построен раз перед началом работы алгоритма)
Для обработки
за нам понадобится следующая структура:- — количество классов;
- — список из номеров классов, содержащихся во множестве ;
- — целочисленный массив, где хранит количество состояний из класса , которые содержатся в ;
- — массив, хранящий в номер нового класса, образовавшегося при разбиении класса .
for push < >, < > into while < > pop from for and if insert in for if //Пометим сразу, т.к. в следующем цикле классы уже будут менятся for and if if move from to in for if for return
Стоит отметить, что массивы
аллоцируются ровно один раз при инициализации алгоритма.Осталось только реализовать
.: if and push < > to else push < > to
Время работы
Лемма (1): |
Количество классов, созданных во время выполнения алгоритма, не превышает . |
Доказательство: |
Представим дерево, которое соответствует операциям разделения классов на подклассы. Корнем этого дерева является все множество состояний | . Листьями являются классы эквивалентности, оставшиеся после работы алгоритма. Так как дерево бинарное — каждый класс может породить лишь два новых, а количество листьев не может быть больше , то количество узлов этого дерева не может быть больше , что доказывает утверждение леммы.
Лемма (2): |
Количество итераций цикла не превышает . |
Доказательство: |
Для доказательства этого утверждения достаточно показать, что количество пар < По > добавленных в очередь не превосходит , так как на каждой итерации мы извлекаем одну пару из очереди. лемме(1) количество классов не превосходит . Пусть элемент текущего разбиения. Тогда количество пар < >, не может быть больше . Отсюда следует, что всего различных пар, которые можно добавить в очередь, не превосходит . |
Лемма (3): |
Пусть и . Тогда количество пар < >, где , которые мы удалим из очереди, не превосходит для фиксированных и . |
Доказательство: |
Рассмотрим пару < | >, где , которую мы удаляем из очереди. И пусть < > следующая пара, где и которую мы удалим из очереди. Согласно нашему алгоритму класс мог появиться в очереди только после операции . Но после первого же разбиения класса на подклассы мы добавим в очередь пару < >, где меньший из образовавшихся подклассов, то есть . Так же заметим, что , а следовательно . Но тогда таких пар не может быть больше, чем .
Лемма (4): |
по всем итерациям цикла не превосходит . |
Доказательство: |
Пусть лемме(3) эта величина не превосходит . Просуммировав по всем и по всем мы получим утверждение леммы. | , и . Зафиксируем эту тройку. Заметим, что количество раз, которое встречается в при условии, что , совпадает с числом удаленных из очереди пар < >, где . Но по
Теорема: |
Время работы алгоритма Хопкрофта равно . |
Доказательство: |
Оценим, сколько времени занимает каждая часть алгоритма:
|
Сравнение с алгоритмом из оригинальной статьи Хопкрофта
В оригинальной статье [1] использовалась дополнительная структура, которую мы обозначим, как , в будем хранить множество состояний, из которых есть ребро по символу в состояние (аналогично , только для классов).
реализуем так:
if and push < > to insert into else push < > to insert into
Циклы
forand (...)
реализуются так:
forand (...)
Тогда время работы внутреннего цикла можно будет оценить как
. А реализация выбирает множество, на котором будет меньшим.Кроме того, вместо хэш-таблиц для хранения множеств ( , разбиение ) можно использовать комбинацию из двусвязного списка и вектора (добавление/удаление через список, поиск через вектор). Что и используется в оригинальной статье.
Источники информации
- Хопкрофт Д., Мотвани Р., Ульман Д. Введение в теорию автоматов, языков и вычислений, 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