задачка (программистское)
Feb. 20th, 2007 03:20 amОтличную задачку прочитал в одном блоге сегодня: написать регулярное выражение (используя только самые стандартные средства регулярных выражений), которое распознает строки, являющиеся двоичной записью чисел, делящихся на 7 (и никакие другие). Пустая строка считается вариантом числа 0 (так что тоже распознается).
Я справился за 104 символа. Если вас заинтересовала задачка и вы ее решили, покажите ваше решение в комментах. Я свое помещу завтра вечером.
P.S. Мое решение в комментах.
no subject
Date: 2007-02-20 02:38 am (UTC)степени двойки
2^i = 1 mod 7 iff i = 0 mod 3
2^i = 2 mod 7 iff i = 1 mod 3
2^i = -3 mod 7 iff i = 2 mod 3
то есть бинарное чисо делится на 7 если записав его в виде
***kjikjikjikji
i + 2j - 3k делится на 7 (то есть i это число единиц стоящих на расстоянии - mod 3 от правого края бинарного представления числа)
например 105 равно
1101001
ikjikji
i = 3
j = 0
k = 1
3 - 3 = 0 делится на 7
далее делаем конечный автомат
прямая реализация
b_ij i = 0..2 j = 0..6
при считываемом нуле b_ij -> b i+1 mod 3,j
при считываемой единице
b i,0 -> b i +1 mod 3, j +1 mod 7
b i,1 -> b i + 1 mod 3, j + 2 mod 7
b i, 2 -> b i +1 mod 3, j -3 mod 7
наверное, больше, чем 104
простите нет времени сейчас до конца доделывать и оптимизировать
no subject
Date: 2007-02-20 03:10 am (UTC)no subject
Date: 2007-02-20 04:01 am (UTC)Только отдельная задница в том, что числа мы записываем в "big-endian" формате, и когда мы видим первую единицу, неизвестно, в какой позиции по модулю 3 она стоит. Чтобы решить эту проблему, нужно отдельно разбирать эти три варианта, что опять же сильно увеличивает размер решения. Видимо, я чего-то не понимаю...
no subject
Date: 2007-02-20 04:48 am (UTC)no subject
Date: 2007-02-20 05:43 am (UTC)no subject
Date: 2007-02-20 06:21 am (UTC)Автомат просто должен честно считать остаток по модулю 7.
При съедании очередной цифры k m:=(m*2+k)%7. Семь состояний, позицию помнить не надо.
no subject
Date: 2007-02-20 06:43 am (UTC)no subject
Date: 2007-02-20 07:44 am (UTC)и я не уверен, что понимаю, как ваш работает.
no subject
Date: 2007-02-20 07:54 am (UTC)Известно, что для проблемы П существует детерминированная машина Тьюринга М, которая решает ее за полиномиальное время
Дать детерминированную машину которая решает П за полиномиальное время (о машине М ничего не известно)
no subject
Date: 2007-02-20 08:06 am (UTC)no subject
Date: 2007-02-20 08:14 am (UTC)no subject
Date: 2007-02-20 09:43 am (UTC)(((((0|00*0)|(1|00*1)1(10*11)*(1|10*0))|((1|00*1)00|(1|00*1)1(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*01(10*11)*(1|10*0))|(((1|00*1)01|(1|00*1)1(10*11)*10*101)|((1|00*1)00|(1|00*1)1(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*((1|00)1|01(10*11)*10*101))((0(10*11)*10*101|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*((1|00)1|01(10*11)*10*101)))*(0(10*11)*(1|10*0)|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*01(10*11)*(1|10*0)))|(((1|00*1)1(10*11)*0|((1|00*1)00|(1|00*1)1(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*01(10*11)*0)|(((1|00*1)01|(1|00*1)1(10*11)*10*101)|((1|00*1)00|(1|00*1)1(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*((1|00)1|01(10*11)*10*101))((0(10*11)*10*101|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*((1|00)1|01(10*11)*10*101)))*(0(10*11)*0|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*01(10*11)*0))((1|0((0(10*11)*10*101|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*((1|00)1|01(10*11)*10*101)))*(0(10*11)*0|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*01(10*11)*0)))*0((0(10*11)*10*101|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*((1|00)1|01(10*11)*10*101)))*(0(10*11)*(1|10*0)|(1|0(10*11)*10*100)(((1|00)0|01(10*11)*10*100))*01(10*11)*(1|10*0)))
На самом деле, есть универсальный способ решать эту задачу для любой системы счисления и для любого делителя.
Если интересно, могу рассказать как, и вывесить программку на JavaScript (аж 70 строчек), которую я использовал для генерации этого регулярного выражения.
no subject
Date: 2007-02-20 10:37 am (UTC)no subject
Date: 2007-02-20 10:44 am (UTC)no subject
Date: 2007-02-20 12:57 pm (UTC)no subject
Date: 2007-02-20 01:23 pm (UTC)a0 + a1 * 2 + a2 * 4 ...
В общем, долго объяснять, к делу. Возьмем число "111" и будем добавлять нули после последней единицы, пытаясь найти закономерность.
Итерация 0: 111 делится на 7, причем число нулей справа не влияет на делимость.
1: 1X101 делится на 7, если вместо X будет стоять (0(0{3}*))
2: 1X1001 - случай более сложный. Один из экстремальных под-случаев: ((001){7})+
Методом подбора получаем два варианта:
11011001 и 01101001. Обобщая:
2a: "1(0{3}*)10(0{3}*)1(0{3}*)100(0{3}*)1" и
2b: "01(0{3}*)10(0{3}*)100(0{3}*)1"
3: (Обобщение 1) Также делится на 7 "1X1X1", где X = "(0{3})*",
4: (Обобщение 2) Также делится на 7 "1X1X1", где X = "(0(0{3})*)",
5: 1X1000001 - частный случай 2.
6: 1X10000001 - частный случай 3.
7: 1X100000001 - частный случай 4.
8: 1X1000000001 - частный случай 2.
9: частный случай 3 и мы зациклились на 3х случаях - 2, 3, 4. Теперь пробуем записать все вместе.
Самая общая запись: (A | B | C | D)+,
где A - (1(0{3}*)10(0{3}*)1(0{3}*)100(0{3}*)1(0)* | 01(0{3}*)10(0{3}*)100(0{3}*)1(0)*) (случай 2);
B - (1(0{3})*1(0{3})*1(0)*) (случай 3);
C - (1(0(0{3})*)1(0(0{3})*)1(0)*) (случай 4).
D - ((001){7}(0)*) (экстремальный случай 2)
В итоге: ((1(0{3}*)10(0{3}*)1(0{3}*)100(0{3}*)1(0)* | 01(0{3}*)10(0{3}*)100(0{3}*)1(0)*) | (1(0{3})*1(0{3})*1(0)*) | (1(0(0{3})*)1(0(0{3})*)1(0)*) | ((001){7}(0)*))+
156 символов, но я не совсем уверен, а проверять времени нет. Может что-то упустил?
(Сокращенная форма:
((1X10X1X100X1(0)* | 01X10X100X1(0)*) | (1X1X1(0)*) | (10X10X1(0)*) ((001){7}(0)*))+,
где X - тройки нулей - (0{3})*)
no subject
Date: 2007-02-20 01:25 pm (UTC)3: (Обобщение 1) Также делится на 7 "1X1X1", где X = "(0{3})*",
4: (Обобщение 2) Также делится на 7 "1X1X1", где X = "(0(0{3})*)",
Следует читать:
3: (Обобщение 0) Также делится на 7 "1X1X1", где X = "(0{3})*",
4: (Обобщение 1) Также делится на 7 "1X1X1", где X = "(0(0{3})*)",
no subject
Date: 2007-02-20 01:37 pm (UTC)no subject
Date: 2007-02-20 02:00 pm (UTC)^(0|1((0(01|111)*(00|110))*(1|0(01|111)*10))(01*0(1(10|000)*(11|0(000)
*01))*(0|1(10|000)*0(000)*1))*1)*$
Кстати, о ^ и $ в начале и конце нельзя забывать, без них неправильно работает по понятным причинам, если задуматься.
Как сделал: сначала немного тормозил :) потом спросил себя, откуда я собственно знаю, что это регулярный язык. Сам себе ответил: потому что легко написать автомат, который проверяет путем просто деления в столбик на 7. Во время деления в столбик на 7 в уме всегда надо помнить не более чем текущий остаток, т.е. у автомата не должно быть более 6 состояний, +-1. Написал автомат. Он очень простой. Состояния обозначаются остатками в скобках, например (010), а переходы в зависимости от следующего символа --0--> или --1-->. Тогда автомат выглядит так (картинку лень рисовать):
(0) --0--> (0)
(0) --1--> (1)
(1) --0--> (10)
(1) --1--> (11)
(10) --0--> (100)
(10) --1--> (101)
ну и так далее. Переходя из трехсимвольных состояний, приписываем цифру, вычитаем 7 и смотрим, куда пришли. Единственный способ вернуться обратно в состояние (0), раз из него выйдя - через (11) --1--> (0).
Дальше из автомата делается регэкс. Это можно сделать автоматически через теорему Клини, но тогда он выходит огромный. Если руками, то можно сделать несколько наблюдений. Нам надо закончить обработку ввода, находясь в состоянии (0) - начальном и конечном. Значит, все выражение будет выглядеть как (0|back-to-zero)* где back-to-zero ловит все возможные способы выйти из 0 и вернуться обратно. Поскольку по дороге обратно мы обязаны пройти через состояние (11) и сделать из него шаг --1-->, можно записать:
back-to-zero = 1(X)(Y)*1
где X ловит все способы добраться от (1) до (11), не проходя уже по дороге через (11), т.е. первый проход до (11); а Y ловит все способы сделать цикл, начинающийся и кончающийся в (11), но не возвращающийся еще в (0). После того, как мы доходим до (1) первой единичкой, оттуда до (11) с помощью X, и крутимся в (11) сколько угодно раз с помощью Y*, мы делаем последний шаг единичкой и возвращаемся в (0).
Ну и дальше X и Y выписываются отдельно похожим образом. В каждом из них мы смотрим на все возможные маршруты, отдельно отмечая циклы (которых всего несколько, так что это просто) и выделяя их в *-ные подстроки. Каждый из них мы по сути дела в уме делим на отдельные случаи и расписываем их так же, как back-to-zero. Это тот же подход, что и в доказательстве теоремы Клини, но из-за того, что мы это делаем на детерминистском автомате и вручную, можно избежать большого количества ненужных *-блоков и повторений. В конце концов выходит
X = (0(01|111)*(00|110))*(1|0(01|111)*10)
Y = 01*0(1(10|000)*(11|0(000)*01))*(0|1(10|000)*0(000)*1)
и из них компонуется ответ.
no subject
Date: 2007-02-20 02:15 pm (UTC)no subject
Date: 2007-02-20 02:19 pm (UTC)Parsing Techniques
Date: 2007-02-20 03:14 pm (UTC)no subject
Date: 2007-02-20 05:10 pm (UTC)который легко доказывается
http://avva.livejournal.com/1727810.html?replyto=40659010
no subject
Date: 2007-02-20 06:20 pm (UTC)no subject
Date: 2007-02-20 11:56 pm (UTC)But JavaScript! That's good. I thought I was the only one that uses JavaScript for casual calculation. :)
no subject
Date: 2007-02-21 01:23 am (UTC)no subject
Date: 2007-02-21 02:56 am (UTC)Эта задачка сейчас достаточно стандартная для начальных курсов по теории автоматов.языков.
еще одна ошибка
Date: 2007-02-21 09:41 am (UTC)