Теорема Клини: регулярки = автоматы

Регулярное выражение и конечный автомат — два разных языка для описания одного и того же класс языков. Это и есть теорема Клини.

Теорема Клини: язык распознаётся конечным автоматом тогда и только тогда, когда он задаётся регулярным выражением. Эти классы совпадают.

Регулярные выражения формально

В теории регулярное выражение строится из трёх базовых операций над языками: объединение (записывается «|» или «+»), конкатенация (просто рядом) и звезда Клини (повтор 0+ раз, «*»). Базовые элементы: символ алфавита, ε (пустое слово), ∅ (пустой язык). Всё, что строится из них, — регулярное выражение, а его язык — регулярный язык. Например, (a|b)*abb — все строки над {a, b}, оканчивающиеся на «abb».

Это та самая теория за практическими regex из курса «Регулярные выражения». Учебные `\d`, `[a-z]+`, `^...$` — синтаксический сахар над этими тремя операциями. А вот «настоящие» возможности вроде обратных ссылок (`\1`) выходят за пределы регулярных языков — о чём ниже.

Две стороны теоремы

Теорема Клини доказывается в обе стороны конструктивно.

Регулярка → автомат (построение Томпсона). По структуре выражения собираем НКА-ε. Символ — два состояния и переход; конкатенация R·S — соединяем выход R с входом S ε-переходом; объединение R|S — новое стартовое состояние с ε-переходами в оба; звезда R* — ε-петля назад и ε-обход вперёд. Вот зачем нужны были ε-переходы из второго раздела — они и есть клей сборки.

R | S  (объединение):                R*  (звезда):

     ε  ┌─► [R] ─┐ ε                 ┌──── ε ────┐
  ──►(s)         (f)──►          ──►(s) ─► [R] ─►(f)──►
     ε  └─► [S] ─┘ ε                 └──── ε ◄───┘

Автомат → регулярка. Обратное направление — метод исключения состояний: по одному убираем состояния автомата, помечая рёбра регулярными выражениями, пока не останутся стартовое и финальное, соединённые единым regex'ом. Так доказывается, что любой ДКА выражается регуляркой.

Проверка регулярки симуляцией автомата

Раз regex эквивалентен автомату, мы можем «исполнить» простое выражение, построив ДКА вручную. Реализуем матчер для (ab)* — строк из повторов «ab».

# ДКА для (ab)* : q0 принимающее, ждём a; q1 ждём b; trap = мёртвое
delta = {
    ("q0","a"):"q1", ("q0","b"):"trap",
    ("q1","a"):"trap", ("q1","b"):"q0",
    ("trap","a"):"trap", ("trap","b"):"trap",
}
def matches_ab_star(w):
    st = "q0"
    for ch in w:
        st = delta[(st, ch)]
    return st == "q0"   # принимающее

for w in ["", "ab", "abab", "aba", "abb", "ba"]:
    print(f"{w or 'ε':>5} -> {matches_ab_star(w)}")

Вывод:

    ε -> True
   ab -> True
 abab -> True
  aba -> False
  abb -> False
   ba -> False

Пустая строка матчится ((ab)* допускает 0 повторов), «abab» матчится, «aba» — нет. Этот ДКА — буквальная компиляция регулярки (ab)*.

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

Промышленные regex-движки делятся на два лагеря. Автоматные (RE2 от Google, движок grep, awk) компилируют регулярку в НКА/ДКА и гарантируют линейное время O(n) от длины строки — потому что просто прогоняют автомат. Backtracking-движки (PCRE, движки в Python, Perl, JavaScript) поддерживают нерегулярные фичи (обратные ссылки), но платят за это: на патологических выражениях они уходят в экспоненциальное время (ReDoS-атаки). Теорема Клини объясняет, почему «честные» регулярные выражения всегда быстры, а расширения — нет.

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

Считать любые «regex» регулярными. Обратные ссылки (`(a+)\1`), рекурсивные шаблоны — НЕ регулярны, они выходят за класс регулярных языков. «Regex» в программировании ≠ «регулярное выражение» в теории.

Думать, что звезда — это «один или более». Звезда Клини — ноль или более; «один или более» — это «+», то есть R·R*.

Забывать ε в (ab)*. Пустая строка входит в язык любой звезды.

Итог

  • Теорема Клини: регулярные выражения и конечные автоматы описывают один и тот же класс — регулярные языки.
  • Регулярка → НКА-ε строится по Томпсону; автомат → регулярка — исключением состояний.
  • Три операции регулярок: объединение, конкатенация, звезда Клини (0+ повторов).
  • Обратные ссылки и рекурсия выходят за класс регулярных языков; автоматные движки за счёт этого работают за O(n).
Проверьте себя
1. Что утверждает теорема Клини?
Aрегулярные выражения мощнее конечных автоматов
Bконечные автоматы и регулярные выражения задают один и тот же класс языков
Cлюбой язык регулярен
Dрегулярки распознают КС-языки
2. Зачем при построении автомата из регулярки нужны ε-переходы?
Aчтобы читать пустые символы из входа
Bчтобы склеивать подавтоматы при конкатенации, объединении и звезде
Cчтобы сделать автомат детерминированным
Dони не нужны
3. Почему обратные ссылки вроде (a+)\1 не описывают регулярный язык?
Aони слишком медленные
Bони выходят за пределы трёх операций регулярных выражений и требуют памяти сверх конечной
Cони не поддерживаются в Python
Dони эквивалентны звезде Клини