Pull to refresh

Comments 26

Круто! Хабр - торт.

Интересно, а можно ли написать автоматический генератор этого удовольствия для разных делителей и систем счисления.

Тут вроде что-то типа линейных уравнений - но можно делать только подстановки и трюк со само-рекуррентностью и надо выразить одну переменную.

Или же есть какой-то алгоритм прямо для перевода конечного детерменированного автомата в регулярное выражение без рекуррентных ссылок.

Или же есть какой-то алгоритм прямо для перевода конечного детерменированного автомата в регулярное выражение без рекуррентных ссылок.

Алгоритм Клини

Интересно, а можно ли написать автоматический генератор этого удовольствия для разных делителей и систем счисления.

На кодоварсе подобная задача есть, для двоичной системы счисления и чисел до 18. Я пока не решал, но там видимо как раз придется такую штуку сделать программно

Ха, вообще алгоритм очень простой получается. Строим автомат, а потом выкидываем по одной вершине, заменяя входящие+исходящие ребра на новые ребра. Главная фишка - теперь на ребрах будем писать регулярные выражения. Если в вершине есть цикл, то он добавляется между входящей и исходящей строкой с *. Если получаются параллельные ребра, то они объединяются через |. Удаляем все вершины, кроме остатка 0. Получившееся в итоге ребро берем в ответ со звездочкой.

Вот регулярка для делимости на 3 для двоичной системы, полученная за 3 минуты на листочке: ^(0|1(01*0)*1)*$

Правда, этой ругулярке будут соответствовать и кривые строки вроде 0011, которые не являются корректным числом, но без ведущих нулей будут длиться на 3. А еще этому соответствует пустая строка. Чтобы этого не было, надо заменить * на + в конце.

Забавно, что обычно для даже очень простого регулярного выражения получается очень большой авотомат, и тот недетерминированный (который можно сделать детерменированным, но он еще больше станет). А тут автомат на самом деле мизерный.

Кстати, задача для проверки делимости на 3 тоже есть на кодоварсе. Да, там всё просто, но некоторые пользователи ухитрились придумать реги, которые зависают на не слишком длинных строках вида "000...01" из-за бэктрекинга. Например, в ответах js видел вот такое:

^(0*(1(01*0)*1)*)*$

Тут же проблема во вложенных, ничем не разделенных *? Не уверен, что можно гарантировать что этот алгоритм не сделает чего-то подобного. Конкретно этой ситуации не будет, ибо звездочка добавляется только для циклов при удалении вершины, и она будет обрамлена двумя ребрами из вершины. И так на каждом ребре.

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

Да получается забавно, что исходной задаче, если ее реализовать в лоб на каком нибудь языке простым автоматом состояний, то ей не нужны никакие ни бектрекинги ни рекурсии, Надо просто идти по графу и определить на какой ноде поток цифр закончится. Но после всех этих преобразований из машины состояний в регулярку и обратно могут рождаться какие то монстры.

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

Да, это любопытный момент. На досуге раскурю, в чем тут философия. Я эти задачки давно видел на cw, но с наскока не взял (кроме делимости на 3, конечно), теперь можно тот 1 kue решить, автоматизировав подход.

Если интересно, вот унивресальное решение для любой базы и делителя.

Проблема только, что ответ она выдает экспоненциально длинный. Пока не смог оценить, но там что-то порядка O(N^B). Хотя в некоторых случаях выдает весьма короткий ответ. Почти половина кода - это разбор случаев, когда там нужно скобки в регекспе ставить.

код
def int_to_base(n, base):
    if n == 0:
        return "0"
    digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    result = ""
    while n > 0:
        n, remainder = divmod(n, base)
        result = digits[remainder] + result
    return result

# Checks if parenthesis are needed when chaining two expressions
def NeedPar(s):
  depth = 0
  for c in s:
    if c == '(': depth += 1
    elif c == ')': depth -= 1
    elif c == '|' and depth == 0: return True
  return False


# Checks if parenthesis are not needed when adding *
def Single(s):
  if len(s) == 1: return True
  if s[0] != '[' or s[-1] != ']': return False
  for i in range(1, len(s)-1):
    if s[i] < '0' or s[i] > int_to_base(B-1, B): return False
  return True



def GenRegEx(N, B):
  tr = [['' for j in range(0, N)] for i in range (0, N)]
  for i in range(0, N):
    for c in range (0, B):
      j = (i*B+c) % N
      tr[i][j] += int_to_base(c, B)
  for i in range(0, N):
    for j in range(0, N):
      if len(tr[i][j]) > 1:
        tr[i][j] = '['+tr[i][j]+']'

  for i in range(N-1, 0, -1):
    for j in range(0, i):
      for k in range (0, i):
        if tr[j][i] == '' or tr[i][k] == '':
          continue
        if NeedPar(tr[j][i]):
          cur = '(' + tr[j][i] + ')'
        else:
          cur = tr[j][i]
        if tr[i][i] != '':
          if Single(tr[i][i]):
            cur += tr[i][i] + '*'
          else:
            cur += '('+tr[i][i] + ')*'
        if NeedPar(tr[i][j]):
          cur += '(' + tr[i][k] + ')'
        else:
          cur += tr[i][k]
        if tr[j][k] != '':
          tr[j][k] += '|'
        tr[j][k] += cur
    for j in range (0, i):
      tr[j].pop()
    tr.pop()
  return '^(' + tr[0][0] + ')*$'

Вот варианты вывода:

N=3 Base=10:
len = 107 
^([0369]|[258][0369]*[147]|([147]|[258][0369]*[258])([0369]|[147][0369]*[258])*([258]|[147][0369]*[147]))*$

N=5 Base=2:
len = 48
^(0|1(10|(0|11)(01*01)*01*00)*(0|11)(01*01)*1)*$

N=3 Base=16:
len=139
^([0369CF]|[258BE][0369CF]*[147AD]|([147AD]|[258BE][0369CF]*[258BE])([0369CF]|[147AD][0369CF]*[258BE])*([258BE]|[147AD][0369CF]*[147AD]))*$

@fori тут получилось даже сильно короче вашего варианта.

Но тут как повезет. Код выше удаляет состояния в порядке убывания. Но если их в другом порядке исключать, то результат может получаться короче или длиннее.

Кажется, пропали некоторые звездочки (например для N=5, B=2), Хабр съедает. Надо не инлайновым кодом обернуть

Отредакировал, спасибо.

Особо обидно, что этот метод для делимости на 10 в десятичной системе выдает регулярку длиной в 567981 знаков.

Надо что-то с комбинаторным взрывом делать и как-то это безобразие минифицировать.

Особо обидно, что этот метод для делимости на 10 в десятичной системе выдает регулярку длиной в 567981 знаков.

для N == Base получается полный граф, где ребро D ведет в вершину D. И под каким-то соусом тут можно все ненулевые вершины схлопнуть в одну ненулевую. Вопрос, в каких ещё случаях можно провернуть такой трюк хотя бы частично...

Плюс, если N раскладывается на множители, то регу можно составить из нескольких lookahead, например при N=12 будет

^(?=..проверка_для_4..$)(?=..проверка_для_3..$)

Хотя для двух разных простых N и Base это всё не сработает, конечно.

Можно в каком-то смысле эквивалентные состояния автомата сжать сначала, да. Но в общем случае это не поможет. Проблема хорошо видна для N=B=3:

^(0 | 22*0 | (1 | 22*1)(1 | 22*1)*(0 | 22*0))*$ 

Тут, фактически перебираются варинты: в строке может быть только цифра 0, цифры 0 и 2, цифры 0,2 и 1. В последнем случае внутри тоже перебираются варинты: цифира 1, цифра 1 и 2 , 0 или 0 и 2.

И все эти случаи можно посокращать, вынося суффиксы-префиксы за скобки.

Я думаю хорошее решение будет строить регулярки не в виде строк, а ввиде дерева операций и оптимизировать, когда ребро удаляется. Там надо будет из всех вариантов через | выделить общие суффиксы/префиксы и все дико посокращается.

Сделал кучу эвристик, вроде

"abc|adc" -> "a(b|d)c"
"(a|)" -> "a?"
"aa*"->"a+"
"a+?"->"a*"

Стало сильно лучше - для N=Base=10 выдало регулярку "всего" в 1538 знаков.

Для N=Base=3 дает теперь сильно более вменяемую регулярку:

^((2*1)*2*0)*$ 

Как ее преобразовывать к

([12]*0)*

я не вижу.

Нужен какой-то принципиально другой подход.

Надо что-то с комбинаторным взрывом делать и как-то это безобразие минифицировать.

В худшем случае (когда n простое) всё равно будет экспоненциальная длина. Регулярка явно прописывает все маршруты из нулевого состояния в нулевое, а их будет много...

Прикол, в этой задаче тупое решение, которое выдает регулярку в 116620 тысяч знаков при N=17 проходит, ибо эвристики, пытающиеся это добро соптимизировать, хоть и выдают регулярку в 83841 знаков, но работают слишком долго.

А тут действительно может быть подвох что более длинное решение не обязательно станет оптимальным. По регулярному выражению строится конечный автомат и всё зависит насколько он станет оптимальным, с учётом что при разборе могут случаться бектрекинги

Если решить исходную задачу деления на 7, то откроется список решений других пользователей и среди них можно найти решение более общей задачи - для любого делителя. Оно оказалось совсем не большое, но код правда на Haskell

Не вполне ясно зачем нужно было избавляться от рекурсии. Рекурсия в регулярках бывает (например, в PCRE есть ?R).

Рекурсия может потребовать стек для хранения переходов, а стек в свою очередь может быть overflow в запущенных случаях. Как минимум от этой проблемы избавимся, так как на выходе получили дерево разбора уложенное в плоскую строку, требующее хранение только текущей позиции в регулярке. Хотя может и ошибаюсь и выполнение простой регулярки без рекурсий тоже хранит стеки

Стек нужен еще для lookbehind.

Отличная статья! Интересное применение для регулярных выражения.

В своё время мне было скучно на занятиях по дискретной математике, где мы писали регулярные выражения и строили конечные автоматы для делимости чисел на 5 или 8 (достаточно просто в десятичной системе счисления). Поэтому, потратив минут 5 я построил автомат (который вышел очень простым), а потом за час построил регулярное выражение для делимости числа на 3 в десятичной системе счисления.

([0369]|[147]([0369]|[147][0369]*[258])*([258]|[147]([0369]|[258][0369]*[147])*[147])|[258]([0369]|[258][0369]*[147])*([147]|[258]([0369]|[147][0369]*[258])*[258]))+

Вот здесь его можно проверить с тестом на первые 300'000 тысяч чисел: https://regex101.com/r/E9CTvt/1

Проверять его нужно не первыми газиллионами, а property-based тестом, сравнивая с

number
|> Integer.digits()
|> Enum.join()
|> String.to_integer()
|> rem(3) == 0

Умные люди придумали QuickCheck почти тридцать лет назад ровно для этой цели.

А сколько тут Integer вернет digits ? А то у to_integer может случится too large or too small for an Int64. Хорошо у Haskell есть теоретически бесконечный Integer (пишут может доходить до 16 ГБ в зависимости от реализации) https://www.reddit.com/r/haskell/comments/1gg3i8/can_haskell_handle_integers_of_any_size_or_is/?rdt=50739

digits возвращает цифры и весь этот код не может вернуть больше (или меньше), чем оригинал numbers, что и проверяется тестом. Следить за переполнением — удел генератора ваших numbers.

Бесконечные Integer есть не только в хаскеле, а еще в руби, эрланге, и эликсире, как минимум.

сам натыкался на эту задачу в кодварсе, меня как большого любителя регулярок она растоптала
автор - гений
задача - хтонь
спасибо большое)

Sign up to leave a comment.

Articles