Пару недель назад, в поисках ответа на задачу, абсолютно не связанную с описываемой здесь, я волею поисковых систем наткнулся на следующий пост: Как сделать из 123456789 число 100 или 0.
Прочитав его, я вспомнил как полгода назад решил такую же, но чуть более глобальную задачу. В этой статье я хотел бы поделиться своим способом решения и дать возможность «поиграться» с алгоритмом. Но сначала немного предыстории.
Давным давно, когда у людей не было смартфонов, в поездках на общественном транспорте каждый развлекал себя как умеет. Одним из таких способов не заскучать была занимательная игра, которая помогала не только скоротать поездку на автобусе, но и немного «расшевелить мозги». Звучит она так. На автобусном билетике есть номер из шести цифр. Необходимо, расставляя между цифрами скобки и знаки математических операций, получить выражение, которое после выполнения операций будет равно 100. Набор операций стандартный: сложение, вычитание, умножение, деление.
Уже не вспомню, кто научил меня этой игре, и кого из своих друзей заразил я, но до сих пор мы периодическилюбили любим так развлекаться. И вот, полгода назад, мне попался следующий пример.
То ли комбинация оказалась сложной, то ли я на тот момент давно не практиковался, но с этим примером я провозился минут 15. В середине этого времени в голове появилась назойливая мысль, что это число вполне может входить в подмножество, для которых решения не существует. Пример я в итоге решил, но осадочек от осознания, что я мог безрезультатно провозиться несколько часов, остался. Кроме того, даже для конкретного числа, доказать такое свойство будет проблематично (конечно, если вы — не профессор теории чисел). Поэтому я задумал решить эту задачу при помощи программирования.
Предупреждение. Автор данной статьи не является профессиональным программистом. Однако, он с благодарностью примет полезные советы по улучшению предложенного алгоритма или его реализации.
Решать поставленную задачу мы будем при помощи перебора. При этом перебирать придётся три различных характеристики решения:
В первом цикле мы будем генерировать очередное разбиение. Для этого представляем произвольное шестизначное число в виде упорядоченного набора цифр с пятью «контейнерами» между ними: 1_2_3_4_5_6. В каждый контейнер мы можем положить либо 1, что будет означать склеивание соседних цифр, либо 0 — соседние цифры будут принадлежать разным числам. Всего существует 2^5 = 32 различных вариантов разбиений, не так уж много. Указанное в примере разбиение кодируется последовательностью 10101.
Перебирать такие варианты можно элементарно — задаём начальное разбиение (00000), и последовательно 31 раз прибавляем к нему единицу в двоичной системе счисления.
Во втором цикле мы имеем N+1 число и N контейнеров между ними. Можно заметить, что количество контейнеров N определяется количеством нулей в коде разбиения. Но более важно здесь осознание того, что в эти новые контейнеры мы должны разложить различные операции из набора (+, -, *, /). В худшем случае N=5, поэтому наихудшая оценка — 4^5 = 1024 варианта расстановки операций.
Перебирать расстановки операций можно способом, аналогичным предыдущему. Кодируем каждую операцию своим ключом (0, 1, 2, 3), задаём начальный набор операций 00000, и последовательно 4^N — 1 раз прибавляем к нему единицу в четверичной системе счисления.
И, наконец, в третьем цикле нужно расставить скобки. Конечно же, делать это совсем «в лоб», расставляя скобки как-нибудь и подсчитывая баланс открывающих и закрывающих, мы не будем. Проще заметить, что для тех же N контейнеров нам всего лишь нужно расставить приоритеты выполнения операций. Имеем N операций, необходимо перебрать все упорядоченные расстановки чисел из набора (0, ..., N). Число всех возможных порядков выполнения будет равно N!, в худшем случае 5! = 120.
Перебирать расстановки приоритетов операций можно перебирая различные перестановки цифр в последовательности (0,1,2,3,4). В своей программе я реализовал какой-то неочевидный алгоритм, последовательно меняющий местами цифры в последовательности. (Если бы писал сейчас, скорее всего, реализовал бы что-то более понятное).
Теперь для решения задачи осталось совсем немного — сделать процедуру, которая по набору из трёх кодов и исходного числа вычисляет результат выражения и в случае если результат равен 100, распечатывает закодированное выражение. Перебор всех вариантов для числа с N+1 цифрой в худшем случае подразумевает рассмотрение 2^N ? 4^N ? N! выражений. Для шестизначных чисел это равно 3.9 миллионов выражений. Оценка получается сильно завышенной, поскольку в выражении 4^N ? N! фактически будет не N, а число нулей в первом разбиении.
В заключении хочу сделать два замечания.
Во-первых, в моей программе сначала использовались стандартные операции над int. Люди же при решении этой задачи работают в поле рациональных чисел, поэтому многие решения, выдаваемые программой на практике нельзя использовать. Спустя какое-то время я написал свой небольшой модуль с рациональными числами и прикрутил к задаче его.
Во-вторых, в моей программе есть зародыш пятой операции — возведения в степень. Причины, по которым эта операция не реализована следующие. Во-первых, необходимо добавлять некоторый ограничитель для возведения в степень (поскольку число 2^2^2^2^2^2, например, уже не влезет в память). Во вторых, в таком случае вычисления выходят из поля рациональных чисел (например, 2^(1/2)).
Программную реализацию предложенного алгоритма на С++ выкладываю на GitHub.
Кроме того, те, кто не хочет заморачиваться с компиляцией, могут скачать исполняемые файлы для Windows там же из релиза.
Прочитав его, я вспомнил как полгода назад решил такую же, но чуть более глобальную задачу. В этой статье я хотел бы поделиться своим способом решения и дать возможность «поиграться» с алгоритмом. Но сначала немного предыстории.
Предыстория
Давным давно, когда у людей не было смартфонов, в поездках на общественном транспорте каждый развлекал себя как умеет. Одним из таких способов не заскучать была занимательная игра, которая помогала не только скоротать поездку на автобусе, но и немного «расшевелить мозги». Звучит она так. На автобусном билетике есть номер из шести цифр. Необходимо, расставляя между цифрами скобки и знаки математических операций, получить выражение, которое после выполнения операций будет равно 100. Набор операций стандартный: сложение, вычитание, умножение, деление.
Уже не вспомню, кто научил меня этой игре, и кого из своих друзей заразил я, но до сих пор мы периодически
То ли комбинация оказалась сложной, то ли я на тот момент давно не практиковался, но с этим примером я провозился минут 15. В середине этого времени в голове появилась назойливая мысль, что это число вполне может входить в подмножество, для которых решения не существует. Пример я в итоге решил, но осадочек от осознания, что я мог безрезультатно провозиться несколько часов, остался. Кроме того, даже для конкретного числа, доказать такое свойство будет проблематично (конечно, если вы — не профессор теории чисел). Поэтому я задумал решить эту задачу при помощи программирования.
Решение и алгоритм
Предупреждение. Автор данной статьи не является профессиональным программистом. Однако, он с благодарностью примет полезные советы по улучшению предложенного алгоритма или его реализации.
Решать поставленную задачу мы будем при помощи перебора. При этом перебирать придётся три различных характеристики решения:
- Различные разбиения исходного числа на подчисла;
- Для каждого разбиения — различные расстановки операций;
- Для каждой расстановки операций — различный порядок выполнения операций (расстановки скобок).
Пример
Пусть у нас есть билетик с номером 123456.
Тогда один из вариантов разбиения на подчисла: 12_34_56.
Вариант расстановки операций: 12+34*56.
Вариант расстановки скобок: (12+34)*56.
Тогда один из вариантов разбиения на подчисла: 12_34_56.
Вариант расстановки операций: 12+34*56.
Вариант расстановки скобок: (12+34)*56.
Первый цикл
В первом цикле мы будем генерировать очередное разбиение. Для этого представляем произвольное шестизначное число в виде упорядоченного набора цифр с пятью «контейнерами» между ними: 1_2_3_4_5_6. В каждый контейнер мы можем положить либо 1, что будет означать склеивание соседних цифр, либо 0 — соседние цифры будут принадлежать разным числам. Всего существует 2^5 = 32 различных вариантов разбиений, не так уж много. Указанное в примере разбиение кодируется последовательностью 10101.
Перебирать такие варианты можно элементарно — задаём начальное разбиение (00000), и последовательно 31 раз прибавляем к нему единицу в двоичной системе счисления.
Второй цикл
Во втором цикле мы имеем N+1 число и N контейнеров между ними. Можно заметить, что количество контейнеров N определяется количеством нулей в коде разбиения. Но более важно здесь осознание того, что в эти новые контейнеры мы должны разложить различные операции из набора (+, -, *, /). В худшем случае N=5, поэтому наихудшая оценка — 4^5 = 1024 варианта расстановки операций.
Перебирать расстановки операций можно способом, аналогичным предыдущему. Кодируем каждую операцию своим ключом (0, 1, 2, 3), задаём начальный набор операций 00000, и последовательно 4^N — 1 раз прибавляем к нему единицу в четверичной системе счисления.
Третий цикл
И, наконец, в третьем цикле нужно расставить скобки. Конечно же, делать это совсем «в лоб», расставляя скобки как-нибудь и подсчитывая баланс открывающих и закрывающих, мы не будем. Проще заметить, что для тех же N контейнеров нам всего лишь нужно расставить приоритеты выполнения операций. Имеем N операций, необходимо перебрать все упорядоченные расстановки чисел из набора (0, ..., N). Число всех возможных порядков выполнения будет равно N!, в худшем случае 5! = 120.
Перебирать расстановки приоритетов операций можно перебирая различные перестановки цифр в последовательности (0,1,2,3,4). В своей программе я реализовал какой-то неочевидный алгоритм, последовательно меняющий местами цифры в последовательности. (Если бы писал сейчас, скорее всего, реализовал бы что-то более понятное).
Проверка комбинации
Теперь для решения задачи осталось совсем немного — сделать процедуру, которая по набору из трёх кодов и исходного числа вычисляет результат выражения и в случае если результат равен 100, распечатывает закодированное выражение. Перебор всех вариантов для числа с N+1 цифрой в худшем случае подразумевает рассмотрение 2^N ? 4^N ? N! выражений. Для шестизначных чисел это равно 3.9 миллионов выражений. Оценка получается сильно завышенной, поскольку в выражении 4^N ? N! фактически будет не N, а число нулей в первом разбиении.
Заключение
В заключении хочу сделать два замечания.
Во-первых, в моей программе сначала использовались стандартные операции над int. Люди же при решении этой задачи работают в поле рациональных чисел, поэтому многие решения, выдаваемые программой на практике нельзя использовать. Спустя какое-то время я написал свой небольшой модуль с рациональными числами и прикрутил к задаче его.
Во-вторых, в моей программе есть зародыш пятой операции — возведения в степень. Причины, по которым эта операция не реализована следующие. Во-первых, необходимо добавлять некоторый ограничитель для возведения в степень (поскольку число 2^2^2^2^2^2, например, уже не влезет в память). Во вторых, в таком случае вычисления выходят из поля рациональных чисел (например, 2^(1/2)).
Ссылки
Программную реализацию предложенного алгоритма на С++ выкладываю на GitHub.
Кроме того, те, кто не хочет заморачиваться с компиляцией, могут скачать исполняемые файлы для Windows там же из релиза.
j_wayne
Ну а 360625 то проверили?!)
WNeZRoS
Никогда про эту игру не слышал, но за минуту раздумий получилось так:
PapaBubaDiop
поскольку являюсь профессионалом в этой игре за полминуты нашел 5 решений
трики — если в конце номерка 5 или 25, то найти 20, 10 или 4 из оставшихся 4 -5 цифр — дело 2-х секунд.
ltvlx Автор
Естественно!
Больше решений богу решений:
((((3-6)*(0-6))+2)*5) = 100
((((3*6)+(0*6))+2)*5) = 100
((((3*6)+(0/6))+2)*5) = 100
((((3*6)-(0*6))+2)*5) = 100
((((3*6)-(0/6))+2)*5) = 100
(((3*6)+((0*6)+2))*5) = 100
(((3*6)+((0/6)+2))*5) = 100
(((3*6)-((0*6)-2))*5) = 100
(((3*6)-((0/6)-2))*5) = 100
((((3+(6*0))*6)+2)*5) = 100
((((3-(6*0))*6)+2)*5) = 100
((((3-6)*(0-6))+2)*5) = 100
((((3*6)+(0*6))+2)*5) = 100
((((3*6)+(0/6))+2)*5) = 100
((((3*6)-(0*6))+2)*5) = 100
((((3*6)-(0/6))+2)*5) = 100
(((3*6)+((0*6)+2))*5) = 100
(((3*6)+((0/6)+2))*5) = 100
(((3*6)-((0*6)-2))*5) = 100
(((3*6)-((0/6)-2))*5) = 100
(((3*((6*0)+6))+2)*5) = 100
(((3*(6+(0*6)))+2)*5) = 100
(((3*(6+(0/6)))+2)*5) = 100
(((3*(6-(0*6)))+2)*5) = 100
(((3*(6-(0/6)))+2)*5) = 100
(((3*6)+((0*6)+2))*5) = 100
(((3*6)+((0/6)+2))*5) = 100
(((3*6)-((0*6)-2))*5) = 100
(((3*6)-((0/6)-2))*5) = 100
((3+((6+0)/6))*25) = 100
((3+((6-0)/6))*25) = 100
((3+(6/(0+6)))*25) = 100
((3-(6/(0-6)))*25) = 100
((3+(6/6))*25) = 100
Denai
Внешние скобки везде зачем?
Writerim
Common Lisp ))
tronix286
Мне нравится множить 1111 на 1111 на калькуляторах -)
Sly_tom_cat
Как то дочке задали задачу из цифр составить результат что-то типа 1?2?3?4?5=6, пробовали, пробовали, я устал, наваял на питоне решалку, нашел сразу 4 варианта решения ее задачи, заодно прошелся по всем соседним числам.
Прикольно оказалось то что после определенного числа решалка делала его из первых нескольких цифр разными знаками, а остальные (до знака равно) шли с попеременными + и — , т.е в итоге давали просто +1 к результату на каждую пару цифр.
Sly_tom_cat
И решал я задачу рекурсивно — если первых знак, к примеру, + то получается:
1 +… = 6 ->… = 5
но получившееся выражение это просто на одну цифру более короткая первичная задача.
Sly_tom_cat
Кстати моя решалка дала такой вариант: (3*6+0+2)*5=100
Serge78rus
Вы вторую шестерку в исходном наборе потеряли 360625
Inine
Она умножилась на ноль.
ivvi
А куда одна шестёрка пропала?
MacIn
Олимпиадная задачка для школьников, помню классе в 4-5 такое решали еще на Бейсике.
mtivkov
Так нет среди первого миллиона чисел тех, для которых нет решения?
ky0
111113
askv
Оно одно такое или ещё есть?
phvoryh
Ещё есть: 000000, 000001, 000002, ..., 000099.
Всего решений нет у 93265 билетов. Если требовать расставить знаки во всех 5 местах, то решений нет у 283730 билетов.
igormu
del
phvoryh
Год назад на хабре уже заходила речь об этой игре, тогда я подсчитал общее количество счастливых таким образом билетов, а заодно попытался сгенерировать наиболее сложные билеты. Если кому-то хочется поломать голову, привожу их список по возрастанию сложности (по моему субъективному мнению). Но в известной мне версии игры необязательно ставить знаки между всеми цифрами, то есть для билета 777777 допустимым решением является (777-77)/7, в примерах это используется.
ltvlx Автор
Классно! Подставил все числа в программу, у каждой оказалось единственное решение.
VovanZ
А есть ли у дьявольского числа
666666
другие решения, кроме((666 - 66) / 6) = 100
?Моя наколенная поделка больше не находит.
hacklex
нет, у 146778 есть ((1+4*6)/(7+7))*8
Программа точно хорошо работает с дробями?
ltvlx Автор
Вы где-то ошиблись.
hacklex
да, Вы правы, в «7+7==2» )))
Deosis
Недавно видел задачу: построить наилучшее приближение числа e, используя все цифры ровно по одному разу. Можно использовать любые операции.
phvoryh
Насколько любые?
Можно написать sinh(1) + cosh(3-2) + 0*(4+5+6+7+8+9) и получить точное значение числа e.
Если позволить факториал, то можно получить сколь угодно близкое к числу e значение, добавляя новые факториалы вокруг (4+5+6) и (7+8):
(1+(3-2)/(4+5+6)!)^(7+8)! + 0*9.
Если использовать степени, то лучшее, что мне удалось найти, это
(1+3^(-2^85))^(9^(4^(6*7))) + 0
Это приближение даёт примерно 1.846*10^25 правильных знаков числа e.
VovanZ
Набросал на коленке очень медленное и весьма говнокодерское решение на питоне. Посмотрим, как быстро pythonanywhere накажет меня за хаброэффект :)
Потыкать онлайн
gist
mikhaelkh
Для числа 146778 не находит решения (14-6/7)*7+8, видимо по-хорошему надо написать класс дроби и считать в нём.
VovanZ
Я и так использую встроенный в питон класс дроби
Оно находит не все решения, т. к. там где-то есть баг в переборе возможных способов расставления скобок.
Но мне уже лень искать и исправлять этот баг :)
andy_p
Все уже придумано давным-давно:
andyplekhanov.narod.ru/science/rnumber.htm
wataru
Оценку комбинаций можно сократить, если считать склейку цифр пятой операцией. Тогда вместо 2^N ? 4^N ? N! = 8^N ? N!, получается 5^N ? N!.. Все-равно завышено, ибо приоритет у склейки всегда будет максимальный, но это уже всего 375000 комбинаций.
Интересно, нашли ли вы хоть одно число, для которого решения нет?
ltvlx Автор
Кстати, изначально я даже планировал так реализовать аглоритм. Когда каждое место можно поставить либо математическую операцию, либо склейку. Однако, для удобства понимания, программирования и отладки, решил их разделить.
Пример чисел без решений привели выше.