Детерминированный конечный автомат (ДКА)

Конечный автомат — простейшая вычислительная машина: горстка состояний и переходы по символам, без всякой памяти, кроме «где я сейчас».

ДКА — детерминированный конечный автомат: пятёрка (Q, Σ, δ, q0, F), где из каждого состояния по каждому символу ведёт ровно один переход.

Пятёрка определения

ДКА формально задаётся пятью компонентами: Q — конечное множество состояний; Σ — входной алфавит; δ: Q × Σ → Q — функция переходов (по состоянию и символу даёт следующее состояние); q0 ∈ Q — начальное состояние; F ⊆ Q — множество принимающих (финальных) состояний.

Работает он так: автомат начинает в q0, читает строку слева направо по одному символу, на каждом шаге переходит по δ в новое состояние. Прочитав всю строку, смотрит на текущее состояние: если оно в F — строка принята, иначе отвергнута. Множество всех принятых строк — язык автомата L(M). Слово «детерминированный» означает: следующий шаг однозначно определён — никакого выбора.

Диаграмма и таблица переходов

Построим ДКА, принимающий двоичные строки, в которых чётное число единиц. Хватит двух состояний: q0 (единиц прочитано чётно — оно же стартовое и принимающее) и q1 (нечётно). Символ 0 не меняет чётность, символ 1 переключает состояние.

        0                 0
       ┌──┐             ┌──┐
       │  v             │  v
    ──>((q0))  --1-->    (q1)
          ^   <--1--      
          └────────────────┘

((q0)) — двойной кружок = принимающее состояние
──>     — стрелка в стартовое состояние

Та же информация в виде таблицы переходов (строки — состояния, столбцы — символы):

состояние01принимающее?
→ q0q0q1да
q1q1q0нет

Симуляция ДКА на Python

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

def run_dfa(delta, start, accept, word):
    state = start
    for ch in word:
        state = delta[(state, ch)]   # ровно один переход
    return state in accept

# ДКА: чётное число единиц
delta = {
    ("q0", "0"): "q0", ("q0", "1"): "q1",
    ("q1", "0"): "q1", ("q1", "1"): "q0",
}
for w in ["", "1", "11", "1010", "111"]:
    ok = run_dfa(delta, "q0", {"q0"}, w)
    print(f"{w or 'ε':>4} -> {'принята' if ok else 'отвергнута'}")

Вывод:

   ε -> принята
   1 -> отвергнута
  11 -> принята
1010 -> принята
 111 -> отвергнута

Пустая строка принята (ноль единиц — чётно), «11» принята, «111» отвергнута — ровно как задумано.

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

Вся «память» ДКА — это его текущее состояние, а состояний конечное число. Значит, автомат различает лишь конечное число ситуаций. Это объясняет его силу и его предел: он распознаёт «чётность», «делимость на 3», «содержит подстроку abc» — всё, что сводится к конечному числу классов, — но не умеет считать до произвольного n. Запомнить «сколько именно единиц» он не может — только «чётно/нечётно». Эта ограниченность — суть будущей леммы о накачке.

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

Забыть переход для какого-то символа. В ДКА δ должна быть определена для каждой пары (состояние, символ). Если перехода нет — это уже не полный ДКА (иногда добавляют «ловушку» — мёртвое состояние).

Перепутать принимающее и стартовое. Стартовое одно, принимающих может быть несколько (или ноль). Состояние может быть и стартовым, и принимающим одновременно.

Думать, что пустую строку нельзя принять. Если q0 ∈ F, то ε принимается — автомат ничего не читает и сразу проверяет стартовое состояние.

Итог

  • ДКА — пятёрка (Q, Σ, δ, q0, F); из каждого состояния по каждому символу ровно один переход.
  • Строка принята, если после её прочтения автомат в принимающем состоянии; язык автомата — все принятые строки.
  • Диаграмма (кружки и стрелки, двойной кружок = принимающее) и таблица переходов — два способа задать δ.
  • Память ДКА — только текущее состояние; считать до произвольного n он не умеет.
Проверьте себя
1. Сколько переходов выходит из состояния ДКА по одному входному символу?
Aноль или один
Bровно один
Cлюбое количество
Dне более числа состояний
2. Когда ДКА принимает строку?
Aесли он прошёл хотя бы одно принимающее состояние
Bесли после прочтения всей строки он в принимающем состоянии
Cесли он не застрял
Dесли строка непустая
3. Почему ДКА не может распознать язык {a<sup>n</sup>b<sup>n</sup>}?
Aв нём слишком мало состояний по умолчанию
Bу него конечная память, он не способен сосчитать произвольное n
Cв нём нет переходов по b
Dон детерминированный