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

Материал из Викиконспекты
Перейти к: навигация, поиск
Строка 14: Строка 14:
 
Если класс <tex>R</tex> может быть разбит по символу <tex>a</tex>, то он содержит хотя бы одну пару неэквивалентных состояний (их можно различить любой строкой начинающейся с символа <tex>a</tex>). Если класс нельзя разбить, то он состоит из эквивалентных состояний.
 
Если класс <tex>R</tex> может быть разбит по символу <tex>a</tex>, то он содержит хотя бы одну пару неэквивалентных состояний (их можно различить любой строкой начинающейся с символа <tex>a</tex>). Если класс нельзя разбить, то он состоит из эквивалентных состояний.
 
Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно.  
 
Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно.  
 +
 +
Итеративно строим разбиение множества состояний следующим образом.
 +
# Первоначальное разбиение множества состояний {{---}} класс допускающих состояний и класс недопускающих состояний.
 +
# Оба этих класса помещаются в очередь.
 +
# Из очереди извлекается класс, далее именуемый как сплиттер.
 +
# Перебираются все символы из алфавита <tex>\Sigma</tex>, где <tex>a</tex> {{---}} текущий символ.
 +
# Все классы текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>a</tex> переходят в сплиттер, а второй из всех оставшихся.
 +
# Те классы, которые разбились на два непустых подкласса, заменяются этими подклассами в разбиении, а также добавляются в очередь.
 +
# Пока очередь не пуста, выполняем п.3 – п.6.
  
 
===Псевдокод===
 
===Псевдокод===
Строка 45: Строка 54:
 
:<tex>\forall r \in B \,\,\, \delta(r, a) \notin R</tex> and <tex> \delta(r, a) \notin R_1</tex>   
 
:<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>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> or
:<tex>\forall r \in B \,\,\, \delta(r, a) \notin R_2</tex>  
+
:<tex> \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/>
Строка 54: Строка 63:
 
:<tex>\forall r \in B \,\,\, \delta(r, a) \notin R_1</tex> and <tex> \delta(r, a) \notin R_2</tex>   
 
:<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>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> or
:<tex>\forall r \in B \,\,\, \delta(r, a) \notin R</tex>  
+
:<tex> \forall r \in B \,\,\, \delta(r, a) \notin R</tex>  
 
Из этого следует, что разбиение всех классов с помощью <tex>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>\Sigma</tex>, где <tex>c</tex> {{---}} текущий символ.
 
# Все классы текущего разбиения разбиваются на 2 подкласса (один из которых может быть пустым). Первый состоит из состояний, которые по символу <tex>c</tex> переходят в сплиттер, а второй из всех оставшихся.
 
# Те классы, которые разбились на два непустых подкласса, заменяются этими подклассами в разбиении, а также меньший из двух подклассов добавляется в очередь.
 
# Пока очередь не пуста, алгоритм выполняет п.3 – п.6.
 
  
 
===Псевдокод===
 
===Псевдокод===
Строка 84: Строка 89:
 
     <tex>W</tex>.pop(<tex>S</tex>)
 
     <tex>W</tex>.pop(<tex>S</tex>)
 
     for all <tex>a \in \Sigma</tex>
 
     for all <tex>a \in \Sigma</tex>
       for all <tex>R</tex> in <tex>P</tex>  
+
       for each <tex>R</tex> in <tex>P</tex> split by <tex>S</tex>
        <tex>R_1 = R \cap \delta^{-1} (S, a) </tex>
+
         replace <tex>R</tex> in <tex>P</tex> with <tex>R_1</tex> and <tex>R_2</tex>
         <tex>R_2 = R \setminus R_1</tex>
+
        if <tex>R</tex> in <tex>W</tex>
        if <tex> |R_1| \ne 0</tex> and <tex>|R_2| \ne 0</tex>
+
            replace <tex>R</tex> in <tex>W</tex> with <tex>R_1</tex> and <tex>R_2</tex>
          replace <tex>R</tex> in <tex>P</tex> with <tex>R_1</tex> and <tex>R_2</tex>
+
          else
          if <tex>R</tex> in <tex>W</tex>
+
            if <tex> |R_1| \le |R_2|</tex>
              replace <tex>R</tex> in <tex>W</tex> with <tex>R_1</tex> and <tex>R_2</tex>
+
                <tex>W</tex>.push(<tex>R_1</tex>)
            else
+
              else
              if <tex> |R_1| \le |R_2|</tex>
+
                <tex>W</tex>.push(<tex>R_2</tex>)
                  <tex>W</tex>.push(<tex>R_1</tex>)
 
                else
 
                  <tex>W</tex>.push(<tex>R_2</tex>)
 
=Корректность алгоритма=
 
 
 
 
 
 
=Время работы алгоритма=
 
=Время работы алгоритма=
 
Благодаря системе добавления классов состояний в очередь, каждое ребро будет рассмотрено не более чем <tex>\log{n}</tex> раз. А так как ребер у нас порядка <tex> |\Sigma| * n </tex> то получаем <tex> O(n\log{n})</tex>
 
Благодаря системе добавления классов состояний в очередь, каждое ребро будет рассмотрено не более чем <tex>\log{n}</tex> раз. А так как ребер у нас порядка <tex> |\Sigma| * n </tex> то получаем <tex> O(n\log{n})</tex>

Версия 00:31, 15 февраля 2012

Постановка задачи

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

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

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

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

Определение:
Класс [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], то он содержит хотя бы одну пару неэквивалентных состояний (их можно различить любой строкой начинающейся с символа [math]a[/math]). Если класс нельзя разбить, то он состоит из эквивалентных состояний. Поэтому самый простой алгоритм состоит в том, чтобы разбивать классы текущего разбиения до тех пор пока это возможно.

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

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

Псевдокод

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

 [math]W \leftarrow \{ F, Q \setminus F \}[/math]
 [math]P \leftarrow \{ F, Q \setminus F \}[/math]
 while not [math]W[/math].isEmpty() 
   [math]W[/math].pop([math]S[/math])
   for all [math]a \in \Sigma[/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] — класс состояний ДКА.

 if [math] |F| \le |Q \setminus F|[/math]
     [math]W[/math].push([math]F[/math])
   else
     [math]W[/math].push([math]Q \setminus F[/math])
 [math]P \leftarrow \{ F, Q \setminus F \}[/math]
 while not [math]W[/math].isEmpty() 
   [math]W[/math].pop([math]S[/math])
   for all [math]a \in \Sigma[/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[/math] in [math]W[/math]
           replace [math]R[/math] in [math]W[/math] with [math]R_1[/math] and [math]R_2[/math]
         else
           if [math] |R_1| \le |R_2|[/math]
               [math]W[/math].push([math]R_1[/math])
             else
               [math]W[/math].push([math]R_2[/math])

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

Благодаря системе добавления классов состояний в очередь, каждое ребро будет рассмотрено не более чем [math]\log{n}[/math] раз. А так как ребер у нас порядка [math] |\Sigma| * n [/math] то получаем [math] O(n\log{n})[/math]

Литература

  • Хопкрофт Д., Мотвани Р., Ульман Д. Введение в теорию автоматов, языков и вычислений, 2-е изд. : Пер. с англ. — М.: Издательский дом «Вильямс», 2002. — С. 177 — ISBN 5-8459-0261-4 (рус.)
  • J. E. Hopcroft. An n log n algorithm for minimizing states in a finite automaton. Technical Report CS-71-190, Stanford University, January 1971.