Наверное, каждый, кто интересуется регулярными выражениями и читает Хабр, видел этот кроссворд из регулярных выражений:   На его решение нужно от 30 минут до нескольких часов, а вот ваш компьютер способен решить его всего за несколько минут. Да и не только этот кроссворд, а любой кроссворд из регулярных выражений под силу алгоритму под катом.  Постараюсь приводить как можно меньше кода — весь код, написанный на Python3, вы найдете на ГитХабе. Аксиома. Если кроссворд из регулярных выражений имеет единственное конечное решение, то все символы, которые встретятся в решении заданы не отрицательными символьными классами и не метасимволом . То есть пусть в неком кроссворде есть только символьные классы: , , ., тогда решение кроссворда будет состоять исключительно из символов A, B и C. Атом. "Атомом" будем называть символьный класс, одиночный литерал или метасимвол символьного класса, которые входят в состав разбираемого регулярного выражения. Один "атом" — один символ в тексте. Понятие "атом" будет использовано, чтобы не путать их с символьными классами внутри регулярного выражения. Например, регулярное выражение [ABC]*ABC[^ABC].\w состоит из семи "атомов" [ABC], A, B, C, [^ABC], . , \w Карта кроссворда, карта. Массив с зависимостями между различными регулярными выражениями для одной ячейки поля кроссворда. Карта содержит сведения, что в данной ячейке поля находится (к примеру) 1-ый символ 3-го регулярного выражения, а также 5-ый символ 8-го регулярного выражения, а также 3-ий символ 12-го регулярного выражения. Проще говоря — в каких позициях регулярные выражения взаимопересекаются. Если попробовать решить кроссворд "в лоб" полным перебором всех символов, то сразу же возникнет проблема, что не получится ждать несколько миллионов лет- слишком много вариантов. И все же- алгоритм представляет собой полный перебор, но не отдельных символов, а "атомов". Для примера, возьмем регулярное выражение .*(IN|SE|HI). В данном кроссворде оно должно представлять собой строку длиной 13 символов. Сначала найдем в регулярном выражении все уникальные "атомы" регулярным выражением: Оно ищет символьные классы, одиночные символы и метасимвол "точка". На текущий момент нет поддержки метасимволов вроде \w, \d и т.п., только самое необходимое. В рассматриваемом выражении мы найдем такие "атомы": "Атомы" необходимо "атомизировать", чтобы рассматривать их строковую запись как неразрывное единое целое, а не отдельные символы: На входе .*(IN|SE|HI), на выходе (?:\.)*((?:I)(?:N)|(?:S)(?:E)|(?:H)(?:I)). Что это даст? Теперь можно делать перебор всех "атомов" на длину 13 "атомов", к полученной строке применять self.regex2 и проверять на соответствие. Для примера: Результатом полного перебора к рассматриваемому регулярному выражению будет массив из трех элементов: Согласитесь, что весьма очевидный результат? Поэкспериментировать с разными регулярными выражениями можно так: В предыдущем шаге было так просто сказано, что регулярное выражение .*(IN|SE|HI) даст всего 3 возможных варианта строки, но ведь на самом деле в выражении 6 "атомов" и длина 13 "атомов", что означает 6 в 13 степени вариантов для перебора. Несколько дней перебора- не самая радужная перспектива, поэтому первая оптимизация по сути только для этого выражения, но на самом деле все оптимизации написаны честным образом — они разбирают регулярное выражение на входе и оптимизируют вычисления, если встретили подходящий шаблон. Выражение .*(IN|SE|HI) оптимизировать весьма легко: Ищем альтернативу в конце строки, проверяем, что все части альтернативы имеют одну длину, выделяем все, что слева от этой альтернативы и обрабатываем отдельно с учётом того, что длина ожидаемого результата уменьшиться на длину части альтернативы. singleRegex( ".*", 11 ) вернет всего один возможный вариант ...........
В цикле проходим по всем возвращенным вариантам и по всем частям альтернативы. Конкатенации возвращаем как результат для исходного регулярного выражения. Время работы сократилось с нескольких дней до 20 минут. Хороший результат, но нужно лучше. Следующая оптимизация очень похожа на первую. Оптимизируем регулярное выражение, которое представлено всего одной альтернативой и квантификатором к альтернативе. В данном кроссворде таких выражений несколько: (DI|NS|TH|OM)*, (O|RHH|MM)*, (E|CR|MN)*, (S|MM|HHH)* Вместо полного перебора напишем несложную рекурсивную функцию: Результат получим за доли секунды, а не за полторы минуты, как до оптимизации. Ещё одна оптимизация применима к большинству регулярных выражений.
Ищем два и более одиночных литерала, не внутри символьного класса  и если до этих литералов не было скобки (. Скобки избегаем, потому что она может означать что угодно: группу, условие, рекурсию. Парсить регулярные выражения регулярными выражениями не лучшая идея, поэтому лучше не заморачиваться сложными оптимизациями. Пусть оптимизируется что может, а что не может- подождём. Забыл добавить, что все оптимизации проходят рекурсивно, поэтому к одному регулярному выражению может быть применено несколько оптимизаций. Рассмотрим выражение [AO]*abc[AO]*def — оно будет оптимизировано в 2 прохода. Сначала заменим abc на , получим [AO]*[a][AO]*def, на втором проходе заменим def на , получим  В результате вместо семи уникальных символьных классов  стало три  — так полный перебор пойдет значительно быстрее, а перед тем как вернуть результат нужно произвести обратные замены  на abc и  на def. Итог оптимизаций: все варианты для всех регулярных выражений считаются примерно 40 секунд. Алгоритм находится в модуле hexaMap.py и рассматривать его смысла нет.
Передаешь ему длину минимальной грани гексагонального поля, а в ответ возвращается карта этого поля. printer — функция, которая получив результат вычислений распечатает решение в соответствии с картой. Регулярное выражение .*(IN|SE|HI) вернет три варианта , разместим их друг под другом, чтобы было понятнее о чем далее идет речь: В нулевой позиции может встретиться только метасимвол ., и так далее вплоть до 10-ой позиции.
В 11-ой позиции может быть три символа I, S, H
В 12-ой позиции может быть три символа N, E, I Создадим сеты (set()) для каждой позиции отдельно — это и есть объединение.
Объединение происходит для каждого регулярного выражения отдельно и показывает в результате какие символы могут быть в определенных позициях в данном регулярном выражении. Метасимволы и символьные классы тоже будут преобразованы в сеты. Помните аксиому? Создадим полный список символов из которых состоит решение кроссворда: Этот полный список соответствует метасимволу ., а отрицательные классы получаются путем вычитания из полного списка символов в отрицательном классе. Для конвертации "атомов" в сеты служит функция: Строчка char = char.lower() делает решение кроссворда регистронезависимым, если потребуется решать регистрозависимый кроссворд, то нужно будет убрать ее и все флаги re.I в коде, но это уже чистый хардкор, а не кроссворд. Результаты объединения просматриваем в соответствии с картой. К примеру, 1-ый символ 3-го регулярного выражения после объединения может содержать {'a', 'b', 'c', 'd'}
5-ый символ 8-го регулярного выражения после объединения может содержать {'a', 'c', 'd', 'x'}
3-ий символ 12-го регулярного выражения после объединения может содержать {'c', 'f', 's'}
Значит, в данной ячейке поля кроссворда может быть только символ c Получив результат пересечения его нужно поместить обратно в результат объединения во все три измерения. Сеты {'a', 'b', 'c', 'd'}, {'a', 'c', 'd', 'x'}, {'c', 'f', 's'} заменяем на сет {'c'}. В предыдущем шаге мы изменили результаты пересечения.
Например в 1-ый символ 3-го регулярного выражения вместо {'a', 'b', 'c', 'd'} вернули {'c'}, соответственно из вариантов нужно удалить те, которые давали в данную позицию {'a', 'b', 'd'}. Если на этом шаге был удален хотя бы один вариант в любом регулярном выражении, то возвращаемся к шагу 4, то есть снова и снова объединяем, пересекаем и фильтруем, пока эти действия приносят плоды. Вот что получим в результате: Звёздочки стоят там, где сеты продолжают содержать несколько значений, а значит — хвала создателям кроссворда, они создали его настолько грамотно, что без анализа обратных ссылок его невозможно решить. Обратные ссылки — единственное, что не было должным образом обработано в вариантах, потому что, например, регулярное выражение (...?)\1* на длину 6 вернет единственный вариант ......, то, что в нем есть обратная ссылка никак не использовалось. Анализ- громкое слово. В игру вновь вступает полный перебор. После полного перебора снова пробуем шаги 4-6 и так до получения результата: Дальше можно решать кроссворды из любых регулярных выражений, какие бы замысловатые конструкции они не содержали, ведь алгоритм честно применяет это "атомизированное" регулярное выражение к "атомам", а значит в нем могут быть все возможные синтаксические конструкции любой сложности. Если в кроссворде есть символьные классы, которые не поддерживаются на текущий момент- добро пожаловать на ГитХаб, ведь для добавления новых символьных классов нужно изменить лишь char2set и reChars. Если хочется, чтобы в кроссворде могли быть символы вроде (, [, то опять же- ГитХаб открыт. Достаточно вместо парсинга регулярных выражений регулярными выражениями строить полноценное синтаксическое дерево регулярного выражения и анализировать его. Спасибо за внимание, надеюсь, что Вы не заскучали от чтения этой небольшой статьи.