Пусть дан автомат, распознающий определенный язык. Требуется найти эквивалентный автомат с наименьшим количеством состояний.
Минимизация ДКА
Если в ДКА существуют два эквивалентных состояния, то при их объединении мы получим эквивалентный ДКА, так как распознаваемый язык не изменится. Основная идея минимизации состоит в разбиении множества состояний на классы эквивалентности, полученные классы и будут состояниями минимизированного ДКА.
Простой алгоритм
Определение: |
Класс [math]C[/math] разбивает класс [math]R[/math] по символу [math]a[/math] на [math]R_1[/math] и [math]R_2[/math], если
- [math]\forall r \in R_1 \,\,\, \delta(r, a) \in C[/math]
- [math]\forall r \in R_2 \,\,\, \delta(r, a) \notin C[/math]
|
Если класс [math]R[/math] может быть разбит по символу [math]a[/math], то он содержит хотя бы одну пару неэквивалентных состояний (так как существует строка которая их различает). Если класс нельзя разбить, то он состоит из эквивалентных состояний.
Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно.
Итеративно строим разбиение множества состояний следующим образом.
- Первоначальное разбиение множества состояний — класс допускающих состояний [math]F[/math] и класс недопускающих состояний ([math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]).
- Перебираются символы алфавита [math]c \in \Sigma[/math], все пары <[math]F,\ c[/math]> и <[math]Q \setminus F, c[/math]> помещаются в очередь.
- Из очереди извлекается пара <[math]C,\ a[/math]>, [math]C[/math] далее именуется как сплиттер.
- Каждый класс [math]R[/math] текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу [math]a[/math] переходят в сплиттер ([math]R_1[/math]), а второй из всех оставшихся ([math]R_2[/math]).
- Если [math]R[/math] разбился на два непустых подкласса (т.е. [math] R_1 \ne \emptyset \ \land \ R_2 \ne \emptyset [/math]).
- В разбиении [math]P[/math] класс [math]R[/math] заменяется на свои подклассы [math]R_1[/math] и [math]R_2[/math].
- Перебираются символы алфавита [math]c \in \Sigma[/math], все пары <[math]R_1, c[/math]> и <[math]R_2, c[/math]> помещаются в очередь.
- Пока очередь не пуста, выполняем п.3 – п.5.
Псевдокод
- [math]Q[/math] — множество состояний ДКА.
- [math]F[/math] — множество терминальных состояний.
- [math]\delta[/math] — функция перехода ([math]\delta (r,\ a)[/math] - состояние, в которое можно совершить переход из [math]r[/math] по символу [math]a[/math])
- [math]S[/math] — очередь пар <[math]C,\ a[/math]>.
- [math]P[/math] — разбиение множества состояний ДКА.
- [math]R[/math] — класс состояний ДКА.
[math]\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)[/math]
[math]\mathtt{P} \leftarrow \{ F,\ Q \setminus F \}[/math]
[math]\mathtt{S} \leftarrow \varnothing [/math]
for [math]c \in \Sigma[/math]
push <[math]F,\ c[/math]>, <[math]Q \setminus F,\ c[/math]> into [math] \mathtt{S}[/math]
while [math]\mathtt{S} \ne \varnothing[/math]
<[math]C,\ a[/math]> [math]\leftarrow[/math] pop from [math]\mathtt{S}[/math]
for [math]R[/math] in [math]\mathtt{P}[/math]
[math] R_1, R_2 \leftarrow [/math] [math]\mathtt{split}(R,\ C,\ a)[/math]
if [math] R_1 \ne \varnothing [/math] and [math] R_2 \ne \varnothing [/math]
replace [math]R[/math] in [math]\mathtt{P}[/math] with [math]R_1[/math] and [math]R_2[/math]
for [math]c \in \Sigma[/math]
insert <[math]R_1,\ c[/math]> in [math]\mathtt{S}[/math]
insert <[math]R_2,\ c[/math]> in [math]\mathtt{S}[/math]
return [math]\mathtt{P}[/math]
Когда очередь [math]S[/math] станет пустой, будет получено разбиение на классы эквивалентности, так как больше ни один класс невозможно разбить.
Время работы
Время работы алгоритма оценивается как [math]O(|\Sigma| \cdot n^2)[/math], где [math] n [/math] — количество состояний ДКА, а [math] \Sigma [/math] — алфавит. Это следует из того, что если пара <[math]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 \ \land \ \delta(r, a) \in R_1[/math]
- [math] \ \lor \ \forall r \in B \,\,\, \delta(r, a) \in R \ \land \ \delta(r, a) \notin R_1[/math]
- [math] \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R \ \land \ \delta(r, a) \notin R_1[/math]
А так как [math]R = R_1 \cup R_2[/math] и [math]R_1 \cap R_2 = \varnothing[/math] то выполняется
- [math]\forall r \in B \,\,\, \delta(r, a) \in R_2 [/math]
- [math] \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R_2[/math]
Из этого следует, что разбиение всех классов с помощью [math]R_2[/math] никак не повлияет на текущее разбиение.
Аналогично доказывается и для разбиения с помощью [math]R [/math] и [math] R_2[/math] по символу [math]a[/math].
Разобьем все классы с помощью [math]R_1[/math] и [math] R_2[/math] по символу [math]a[/math], тогда для любого класса [math]B[/math] из текущего разбиения выполняется
- [math]\forall r \in B \,\,\, \delta(r, a) \in R_1 \ \land \ \delta(r, a) \notin R_2[/math]
- [math] \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R_1 \ \land \ \delta(r, a) \in R_2[/math]
- [math] \ \lor \ \forall r \in B \,\,\, \delta(r, a) \notin R_1 \ \land \ \delta(r, a) \notin R_2[/math]
А так как [math]R = R_1 \cup R_2[/math] и [math]R_1 \cap R_2 = \varnothing[/math] то выполняется
- [math]\forall r \in B \,\,\, \delta(r, a) \in R [/math]
- [math] \ \lor \ \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]\mathtt{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]\delta[/math] — функция перехода ([math]\delta (r,\ a)[/math] - состояние, в которое можно совершить переход из [math]r[/math] по символу [math]a[/math])
- [math]S[/math] — очередь пар <[math]C,\ a[/math]>.
- [math]P[/math] — разбиение множества состояний ДКА.
- [math]R[/math] — класс состояний ДКА.
[math]\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)[/math]
[math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
[math]\mathtt{S} \leftarrow \varnothing [/math]
for [math]c \in \Sigma[/math]
push <[math]F,\ c[/math]>, <[math]Q \setminus F,\ c[/math]> into [math] \mathtt{S}[/math]
while [math]\mathtt{S} \ne \varnothing[/math]
<[math]C,\ a[/math]> [math]\leftarrow[/math] pop from [math]\mathtt{S}[/math]
for [math]R[/math] in [math]\mathtt{P}[/math]
[math] R_1, R_2 \leftarrow [/math] [math]\mathtt{split}(R,\ C,\ a)[/math]
if [math] R_1 \ne \varnothing [/math] and [math] R_2 \ne \varnothing [/math]
replace [math]R[/math] in [math]\mathtt{P}[/math] with [math]R_1[/math] and [math]R_2[/math]
for [math]c \in \Sigma[/math]
[math]\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2,\ c)[/math]
return [math]\mathtt{P}[/math]
Понятно, что нам нет никакой необходимости просматривать все классы в разбиении. Вполне достаточно рассмотреть лишь те классы, из состояний которых есть хотя бы одно ребро в состояния сплиттера. Обозначим множество таких классов за T' (его нужно будет эффективно находить для каждой пары <[math]C,\ a[/math]>).
[math]\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)[/math]
[math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
[math]\mathtt{S} \leftarrow \varnothing [/math]
for [math]c \in \Sigma[/math]
push <[math]F,\ c[/math]>, <[math]Q \setminus F,\ c[/math]> into [math] \mathtt{S}[/math]
while [math]\mathtt{S} \ne \varnothing[/math]
<[math]C,\ a[/math]> [math]\leftarrow[/math] pop from [math]\mathtt{S}[/math]
[math]\mathtt{Inverse} \leftarrow \{r \ | \ r \in Q, \ \delta(r, a) \in C\}[/math]
[math]T' \leftarrow \{R \ | \ R \in \mathtt{P}, \ R \cap \mathtt{Inverse} \neq \varnothing\}[/math]
for [math]R[/math] in [math]T'[/math]
[math] R_1, R_2 \leftarrow [/math] [math]\mathtt{split}(R,\ C,\ a)[/math]
if [math] R_1 \ne \varnothing [/math] and [math] R_2 \ne \varnothing [/math]
replace [math]R[/math] in [math]\mathtt{P}[/math] with [math]R_1[/math] and [math]R_2[/math]
for [math]c \in \Sigma[/math]
[math]\mathtt{pushSetsToQueue}(S,\ R_1,\ R_2,\ c)[/math]
return [math]\mathtt{P}[/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}[/math] — двумерный массив булеанов, [math]\mathtt{InQueue}[C][a] == true[/math], если <[math]C,\ a[/math]> находится в очереди [math]\mathtt{Queue}[/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]\mathtt{findEquivalenceClasses}(Q,\ F,\ \delta)[/math]
[math]\mathtt{P} \leftarrow \{ F, \ Q \setminus F \}[/math]
for [math]c \in \Sigma[/math]
push <[math]F,\ c[/math]>, <[math]Q \setminus F,\ c[/math]> into [math] \mathtt{Queue}[/math]
[math]\mathtt{InQueue}[F][c] \ \leftarrow \ true[/math]
[math]\mathtt{InQueue}[Q \setminus F][c] \ \leftarrow \ true[/math]
while [math]\mathtt{Queue} \ne \varnothing[/math]
<[math]C,\ a[/math]> [math]\leftarrow[/math] pop from [math]\mathtt{Queue}[/math]
[math]\mathtt{Involved} \leftarrow \varnothing[/math]
for [math]q \in C[/math] and [math]r \in \mathtt{Inv}[q][a][/math]
[math]i = \mathtt{Class}[r][/math]
if [math]\mathtt{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] in [math]\mathtt{P}[/math]
for [math] j \in \mathtt{Involved}[/math]
if [math] \mathtt{Twin}[j] \neq 0 [/math]
for [math]c \in \Sigma[/math]
[math]\mathtt{pushSetsToQueue}(\mathtt{Queue},\ j,\ \mathtt{Twin}[j],\ c)[/math]
[math]\mathtt{Size}[j] = 0[/math]
[math]\mathtt{Twin}[j] = 0[/math]
[math]\mathtt{InQueue}[C][a] \ \leftarrow \ false[/math]
return [math]\mathtt{P}[/math]
Стоит отметить, что массивы [math]\mathtt{Size}, \mathtt{Twin}[/math] аллоцируются ровно один раз при инициализации алгоритма.
Осталось только реализовать [math]\mathtt{pushSetsToQueue}[/math].
[math]\mathtt{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] \mathtt{InQueue}[R_1][c] == false [/math] and [math] cnt1 \leqslant cnt2 [/math]
push <[math]R_1, c[/math]> to [math]\mathtt{Queue}[/math]
[math]\mathtt{InQueue}[R_1][c] \ \leftarrow \ true[/math]
else
push <[math]R_2, c[/math]> to [math]\mathtt{Queue}[/math]
[math]\mathtt{InQueue}[R_2][c] \ \leftarrow \ true[/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[/math]>, [math]\ a \in \Sigma[/math] не может быть больше [math]|\Sigma|[/math]. Отсюда следует, что всего различных пар, которые можно добавить в очередь, не превосходит [math] 2 |\Sigma| |Q| [/math]. |
[math]\triangleleft[/math] |
Лемма (3): |
Пусть [math]a \in \Sigma[/math] и [math]p \in Q[/math]. Тогда количество пар <[math]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] |
Сравнение с алгоритмом из оригинальной статьи Хопкрофта
В оригинальной статье [1] использовалась дополнительная структура, которую мы обозначим, как [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 \ \land \ \delta^{-1} (s, a) \neq \emptyset \}[/math]
[math]\mathtt{pushSetsToQueue}[/math] реализуем так:
[math]\mathtt{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] \mathtt{InQueue}[R_1][c] == false [/math] and [math] cnt1 \leqslant 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]
(...)
реализуются так:
for [math]q \in \mathtt{ClassInv}[C][a][/math] and [math]r \in \mathtt{Inv}[q][a][/math]
(...)
Тогда время работы внутреннего цикла можно будет оценить как [math]O(|\mathtt{ClassInv}[C][a]| + |\mathtt{Inverse}|)[/math]. А реализация [math]\mathtt{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.
- John Hopcroft An O(nlogn) algorithm for minimizing states in a finite automation
Примечания