Магазинные автоматы (PDA)

Магазинный автомат — это конечный автомат, которому выдали стек. Ровно этой добавки хватает, чтобы распознавать контекстно-свободные языки.

Магазинный автомат (pushdown automaton, PDA) — конечный автомат, дополненный неограниченным стеком; на каждом шаге он читает символ входа, смотрит вершину стека и может класть/снимать символы стека.

Зачем стек

Конечный автомат провалился на anbn и скобках, потому что не мог «считать». Стек решает проблему: на каждой a кладём метку на стек, на каждой b — снимаем. Если в конце стек пуст, значит a и b было поровну. Стек — неограниченная память, но с дисциплиной LIFO (последним вошёл — первым вышел), что идеально подходит вложенности. Теорема: класс языков, распознаваемых магазинными автоматами, совпадает с классом КС-языков. PDA — машинный аналог КС-грамматики, как конечный автомат — аналог регулярки.

Как устроен переход

Переход PDA учитывает тройку: текущее состояние, входной символ (или ε) и символ на вершине стека. Результат — новое состояние и что сделать со стеком (снять вершину, заменить, добавить). PDA по определению недетерминирован, и это важно: недетерминированные PDA строго мощнее детерминированных (DPDA распознают меньший класс — на нём строят однозначные парсеры).

PDA для a^n b^n (n ≥ 1), стек с маркером дна Z:

фаза a:  (q0, a, Z) → (q0, AZ)    положить A
         (q0, a, A) → (q0, AA)    ещё A
фаза b:  (q0, b, A) → (q1, ε)     снять A (первая b)
         (q1, b, A) → (q1, ε)     снимать A
конец:   (q1, ε, Z) → (qf, Z)     стек пуст (только Z) ⇒ принять

идея: каждая a кладёт A, каждая b снимает A. Сошлось ⇒ принято.

Симуляция PDA на Python

Реализуем детерминированный PDA для anbn со стеком-списком. Кладём метку на a, снимаем на b, в конце проверяем пустоту.

def pda_anbn(word):
    stack = []
    phase = "a"          # сначала ждём a, потом b
    for ch in word:
        if ch == "a":
            if phase != "a":
                return False     # a после b — нельзя
            stack.append("A")    # push
        elif ch == "b":
            phase = "b"
            if not stack:
                return False     # b без парной a
            stack.pop()          # pop
        else:
            return False
    return len(stack) == 0 and phase == "b" or word == ""

for w in ["ab", "aabb", "aaabbb", "aab", "abb", "ba", "aabbb"]:
    print(f"{w:>7} -> {'принято' if pda_anbn(w) else 'отвергнуто'}")

Вывод:

     ab -> принято
   aabb -> принято
 aaabbb -> принято
    aab -> отвергнуто
    abb -> отвергнуто
     ba -> отвергнуто
  aabbb -> отвергнуто

«aab» отвергнуто (a больше, чем b — стек не пуст), «abb» отвергнуто (b больше — стек опустел рано). Стек сделал то, что конечному автомату не под силу: сосчитал и сравнил.

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

Почему один стек, а не два? PDA с двумя стеками эквивалентен машине Тьюринга — он мощнее и распознаёт уже не КС-языки, а всё вычислимое (два стека имитируют ленту). Один стек — золотая середина: достаточно для вложенности (скобки, выражения, разметка), но не для «тройного» подсчёта anbncn (это уже не КС). Практические парсеры — это, по сути, детерминированные PDA: LR-парсер держит стек состояний и символов и принимает решения «push/pop/reduce», что прямо соответствует переходам PDA.

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

Забыть проверить пустоту стека в конце. Принятие требует не только дочитать вход, но и согласованного состояния стека (часто — его опустошения).

Считать DPDA и NPDA одинаковыми. Недетерминированный PDA мощнее: например, язык палиндромов {wwR} распознаётся только NPDA (нужно «угадать» середину).

Думать, что стек даёт всё. Один стек = КС-языки, не больше. anbncn стеку не по зубам — нужна лента (Тьюринг).

Итог

  • Магазинный автомат — конечный автомат + неограниченный стек (LIFO); переход учитывает вход и вершину стека.
  • Класс языков PDA в точности совпадает с КС-языками; PDA — машинный аналог КС-грамматики.
  • Недетерминированные PDA мощнее детерминированных (палиндромы — только NPDA).
  • Один стек = КС; два стека уже эквивалентны машине Тьюринга.
Проверьте себя
1. Чем магазинный автомат отличается от конечного?
Aу него больше состояний
Bу него есть неограниченный стек (LIFO-память)
Cон всегда детерминирован
Dон читает вход справа налево
2. Какой класс языков распознают магазинные автоматы?
Aрегулярные
Bконтекстно-свободные
Cвсе рекурсивно-перечислимые
Dтолько конечные
3. Что произойдёт, если дать магазинному автомату два стека вместо одного?
Aон станет слабее
Bон станет эквивалентен машине Тьюринга
Cничего не изменится
Dон перестанет распознавать скобки