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

У каждого регулярного языка есть единственный самый маленький ДКА. Минимизация находит его, слив неразличимые состояния.

Минимизация ДКА — построение эквивалентного автомата с наименьшим числом состояний путём слияния состояний, неразличимых по поведению.

Когда два состояния «одинаковы»

Два состояния p и q называются эквивалентными, если для любой строки w автомат, стартовав из p или из q, одинаково решает — принять или отвергнуть. Проще: из p и q нельзя «различить» язык. Такие состояния — дубликаты, их можно слить в одно без изменения языка. Минимальный ДКА получается, когда все дубликаты слиты. Замечательный факт: для каждого регулярного языка минимальный ДКА единственен с точностью до переименования состояний (теорема Майхилла-Нероуда). Это делает минимальный ДКА канонической формой языка — по нему можно проверять эквивалентность двух автоматов.

Алгоритм разбиения

Классический алгоритм Хопкрофта работает «сверху вниз», уточняя разбиение состояний на классы:

  1. Начальное разбиение — два класса: принимающие F и непринимающие Q\F (их точно нельзя слить — ε их различает).
  2. Берём класс и символ a. Если внутри класса разные состояния по a уходят в разные классы — этот класс надо расщепить.
  3. Повторяем, пока разбиение не перестанет меняться.
  4. Каждый финальный класс — одно состояние минимального ДКА.
Σ={a,b}. Состояния: q0,q1,q2,q3. Принимающее: q3.
Начало: {q0,q1,q2} | {q3}
Проверяем по символам, какие уходят в класс {q3} → расщепляем,
пока классы стабильны. Эквивалентные состояния схлопываются.

Минимизация на Python

Реализуем разбиение состояний. Возьмём ДКА с заведомым дубликатом и убедимся, что он схлопнется.

def minimize(states, alphabet, delta, accept):
    # начальное разбиение: принимающие и остальные
    P = [set(accept), set(states) - set(accept)]
    P = [b for b in P if b]
    def class_of(s, part):
        for i, b in enumerate(part):
            if s in b:
                return i
    changed = True
    while changed:
        changed = False
        newP = []
        for block in P:
            groups = {}
            for s in block:
                sig = tuple(class_of(delta[(s, c)], P) for c in alphabet)
                groups.setdefault(sig, set()).add(s)
            if len(groups) > 1:
                changed = True
            newP.extend(groups.values())
        P = newP
    return P

states = ["A", "B", "C", "D"]
# B и C ведут себя одинаково (дубликаты)
delta = {
    ("A","0"):"B", ("A","1"):"C",
    ("B","0"):"D", ("B","1"):"D",
    ("C","0"):"D", ("C","1"):"D",
    ("D","0"):"D", ("D","1"):"D",
}
classes = minimize(states, "01", delta, {"D"})
print("классов (состояний минимального ДКА):", len(classes))
for c in sorted(map(sorted, classes)):
    print(c)

Вывод:

классов (состояний минимального ДКА): 3
['A']
['B', 'C']
['D']

Состояния B и C слились в один класс — они вели себя идентично. Из 4 состояний осталось 3: это и есть минимальный ДКА.

Как работает под капотом

Почему минимум единственен? Теорема Майхилла-Нероуда связывает состояния минимального ДКА с классами эквивалентности строк: две строки эквивалентны, если их любым одинаковым продолжением нельзя различить (обе ведут в L или обе нет). Число таких классов — индекс языка. Если он конечен — язык регулярен, и минимальный ДКА имеет ровно столько состояний, сколько классов. Это даёт ещё один критерий не-регулярности: если классов бесконечно много (как у anbn, где каждое an — свой класс), язык не регулярен.

Частые ошибки

Минимизировать НКА. Алгоритм работает на ДКА. Минимальный НКА — вообще трудная (PSPACE) задача и не единственен. Сначала детерминизируйте.

Забыть про недостижимые состояния. Перед минимизацией удалите состояния, недостижимые из старта — иначе они засорят результат.

Слить принимающее с непринимающим. Они различимы пустой строкой, поэтому стартовое разбиение всегда отделяет F от остальных.

Итог

  • Эквивалентные состояния неразличимы никакой строкой — их можно слить без изменения языка.
  • Алгоритм минимизации уточняет разбиение состояний, начиная с {принимающие} | {остальные}.
  • Минимальный ДКА единственен (Майхилл-Нероуд) — это каноническая форма регулярного языка.
  • Число классов эквивалентности конечно ⇔ язык регулярен; бесконечно ⇒ не регулярен.
Проверьте себя
1. Когда два состояния ДКА можно слить при минимизации?
Aкогда у них одинаковое имя
Bкогда ни одна строка не различает их (оба принимают/отвергают одинаково)
Cкогда из них выходит одинаковое число переходов
Dкогда они оба непринимающие
2. Сколько минимальных ДКА у одного регулярного языка?
Aбесконечно много
Bровно один с точностью до переименования состояний
Cпо одному на каждое регулярное выражение
Dни одного
3. С какого разбиения начинается минимизация ДКА?
Aвсе состояния в одном классе
B{принимающие} и {непринимающие} как два класса
Cкаждое состояние в своём классе
Dслучайное разбиение