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

Материал из Викиконспекты
Перейти к: навигация, поиск
(Псевдокод)
(Время работы алгоритма)
Строка 102: Строка 102:
  
 
==Время работы алгоритма==
 
==Время работы алгоритма==
Время работы алгоритма равно <tex>O(|\Sigma| * n\log{n})</tex>, где <tex> n </tex> {{---}} количество состояний ДКА, а <tex> \Sigma </tex>{{---}} алфавит. Это следует из того, что каждое из ребер, а их порядка <tex> |\Sigma| * n </tex>, участвует не более чем в <tex> \log{n}</tex> разбиениях.
+
Время работы алгоритма оценивается как <tex>O(|\Sigma| * n\log{n})</tex>, где <tex> n </tex> {{---}} количество состояний ДКА, а <tex> \Sigma </tex>{{---}} алфавит. Это следует из того, что если пара <tex>(S, a)</tex> попала в очередь, и класс <tex>S</tex> использовася в качестве сплиттера, то при последующем разбиении этого класса в очередь будет добавлен класс <tex>S_1</tex>, причем <tex>|S| \ge 2|S_1|</tex>. Каждое состояние изначально принадлежит лишь одному классу в очереди, тогда из вышеуказанного следует, что каждый переход в автомате будет просмотрен не более, чем <tex>O(\log{n})</tex> раз. Учитывая, что ребер всего <tex>O(|\Sigma| * n)</tex>, получаем указанную оценку.
  
 
== Литература ==
 
== Литература ==

Версия 18:22, 15 января 2013

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

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

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

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

Определение:
Класс [math]S[/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 S[/math]
  2. [math]\forall r \in R_2 \,\,\, \delta(r, a) \notin S[/math]

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

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

  1. Первоначальное разбиение множества состояний — класс допускающих состояний [math]Q[/math] и класс недопускающих состояний [math]Q \setminus F[/math].
  2. Перебираются символы алфавита [math]a \in \Sigma[/math], все пары [math](Q, a)[/math] и [math](Q \setminus F, a)[/math] помещаются в очередь.
  3. Из очереди извлекается пара [math](S, a)[/math], [math]S[/math] далее именуется как сплиттер.
  4. Все классы текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу [math]a[/math] переходят в сплиттер, а второй из всех оставшихся.
  5. Те классы, которые разбились на два непустых подкласса, заменяются этими подклассами в разбиении, а также добавляются в очередь.
  6. Пока очередь не пуста, выполняем п.3 – п.5.

Псевдокод

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

 [math]P \leftarrow \{ F, Q \setminus F \}[/math]
 [math]W \leftarrow \{ \}[/math]
 for all [math]a \in \Sigma[/math]
   [math]W[/math].push([math]F, a[/math])
   [math]W[/math].push([math]Q \setminus F, a[/math])
 while not [math]W[/math].isEmpty() 
   [math]W[/math].pop([math]S, a[/math])
   for all [math]R[/math] in [math]P[/math] 
     [math]R_1 = R \cap \delta^{-1} (S, a) [/math]
     [math]R_2 = R \setminus R_1[/math]
     if [math] |R_1| \ne 0[/math] and [math]|R_2| \ne 0[/math]
       replace [math]R[/math] in [math]P[/math] with [math]R_1[/math] and [math]R_2[/math]
       [math]W[/math].push([math]R_1[/math])
       [math]W[/math].push([math]R_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]R_1[/math] и [math]R_2[/math]. Если класса [math]R[/math] нет в очереди, то согласно лемме в очередь можно добавить класс [math]R[/math] и любой из [math]R_1[/math] и [math]R_2[/math], а так как для любого класса [math]B[/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_1[/math] и [math]R_2[/math].

Псевдокод

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

 [math]W \leftarrow \{ \}[/math]
 if [math] |F| \le |Q \setminus F|[/math]
   for all [math]a \in \Sigma[/math]
     [math]W[/math].push([math]F, a[/math])
 else
   for all [math]a \in \Sigma[/math]
     [math]W[/math].push([math]Q \setminus F, a[/math])
 [math]P \leftarrow \{ F, Q \setminus F \}[/math]
 while not [math]W[/math].isEmpty() 
   [math]W[/math].pop([math]S, a[/math])
   for each [math]R[/math] in [math]P[/math] split by [math]S[/math]  
     replace [math]R[/math] in [math]P[/math] with [math]R_1[/math] and [math]R_2[/math]
     if ([math]R, a[/math]) in [math]W[/math]
       replace ([math]R, a[/math]) in [math]W[/math] with ([math]R_1, a[/math]) and ([math]R_2, a[/math])
     else
       if [math] |R_1| \le |R_2|[/math]
         [math]W[/math].push([math]R_1, a[/math])
       else
         [math]W[/math].push([math]R_2, a[/math])

Время работы алгоритма

Время работы алгоритма оценивается как [math]O(|\Sigma| * n\log{n})[/math], где [math] n [/math] — количество состояний ДКА, а [math] \Sigma [/math]— алфавит. Это следует из того, что если пара [math](S, a)[/math] попала в очередь, и класс [math]S[/math] использовася в качестве сплиттера, то при последующем разбиении этого класса в очередь будет добавлен класс [math]S_1[/math], причем [math]|S| \ge 2|S_1|[/math]. Каждое состояние изначально принадлежит лишь одному классу в очереди, тогда из вышеуказанного следует, что каждый переход в автомате будет просмотрен не более, чем [math]O(\log{n})[/math] раз. Учитывая, что ребер всего [math]O(|\Sigma| * n)[/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.