Пару недель назад, в поисках ответа на задачу, абсолютно не связанную с описываемой здесь, я волею поисковых систем наткнулся на следующий пост: Как сделать из 123456789 число 100 или 0.

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

Предыстория


Давным давно, когда у людей не было смартфонов, в поездках на общественном транспорте каждый развлекал себя как умеет. Одним из таких способов не заскучать была занимательная игра, которая помогала не только скоротать поездку на автобусе, но и немного «расшевелить мозги». Звучит она так. На автобусном билетике есть номер из шести цифр. Необходимо, расставляя между цифрами скобки и знаки математических операций, получить выражение, которое после выполнения операций будет равно 100. Набор операций стандартный: сложение, вычитание, умножение, деление.

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



То ли комбинация оказалась сложной, то ли я на тот момент давно не практиковался, но с этим примером я провозился минут 15. В середине этого времени в голове появилась назойливая мысль, что это число вполне может входить в подмножество, для которых решения не существует. Пример я в итоге решил, но осадочек от осознания, что я мог безрезультатно провозиться несколько часов, остался. Кроме того, даже для конкретного числа, доказать такое свойство будет проблематично (конечно, если вы — не профессор теории чисел). Поэтому я задумал решить эту задачу при помощи программирования.

Решение и алгоритм


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

Решать поставленную задачу мы будем при помощи перебора. При этом перебирать придётся три различных характеристики решения:

  1. Различные разбиения исходного числа на подчисла;
  2. Для каждого разбиения — различные расстановки операций;
  3. Для каждой расстановки операций — различный порядок выполнения операций (расстановки скобок).

Пример
Пусть у нас есть билетик с номером 123456.
Тогда один из вариантов разбиения на подчисла: 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 там же из релиза.

Комментарии (33)


  1. j_wayne
    09.10.2017 11:43

    Ну а 360625 то проверили?!)


    1. WNeZRoS
      09.10.2017 11:52
      +2

      Никогда про эту игру не слышал, но за минуту раздумий получилось так:

      ответ
      (3 + (6 + 0) / 6) * 25


      1. PapaBubaDiop
        09.10.2017 12:24
        +1

        поскольку являюсь профессионалом в этой игре за полминуты нашел 5 решений

        трики — если в конце номерка 5 или 25, то найти 20, 10 или 4 из оставшихся 4 -5 цифр — дело 2-х секунд.


    1. ltvlx Автор
      09.10.2017 19:29
      +1

      Естественно!
      Больше решений богу решений:

      ((((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


      1. Denai
        09.10.2017 20:19

        Внешние скобки везде зачем?


        1. Writerim
          10.10.2017 07:46

          Common Lisp ))


  1. tronix286
    09.10.2017 12:24
    +1

    Мне нравится множить 1111 на 1111 на калькуляторах -)


  1. Sly_tom_cat
    09.10.2017 14:17
    +1

    Как то дочке задали задачу из цифр составить результат что-то типа 1?2?3?4?5=6, пробовали, пробовали, я устал, наваял на питоне решалку, нашел сразу 4 варианта решения ее задачи, заодно прошелся по всем соседним числам.

    Прикольно оказалось то что после определенного числа решалка делала его из первых нескольких цифр разными знаками, а остальные (до знака равно) шли с попеременными + и — , т.е в итоге давали просто +1 к результату на каждую пару цифр.


    1. Sly_tom_cat
      09.10.2017 14:25

      И решал я задачу рекурсивно — если первых знак, к примеру, + то получается:
      1 +… = 6 ->… = 5
      но получившееся выражение это просто на одну цифру более короткая первичная задача.


      1. Sly_tom_cat
        09.10.2017 14:47

        Кстати моя решалка дала такой вариант: (3*6+0+2)*5=100


        1. Serge78rus
          09.10.2017 15:57

          Вы вторую шестерку в исходном наборе потеряли 360625


          1. Inine
            09.10.2017 16:14
            +2

            Она умножилась на ноль.


        1. ivvi
          09.10.2017 16:12

          А куда одна шестёрка пропала?


  1. MacIn
    09.10.2017 17:24

    Олимпиадная задачка для школьников, помню классе в 4-5 такое решали еще на Бейсике.


  1. mtivkov
    09.10.2017 18:57

    Так нет среди первого миллиона чисел тех, для которых нет решения?


    1. ky0
      10.10.2017 01:33
      +1

      111113


      1. askv
        10.10.2017 08:11

        Оно одно такое или ещё есть?


        1. phvoryh
          10.10.2017 08:35
          +1

          Ещё есть: 000000, 000001, 000002, ..., 000099.
          Всего решений нет у 93265 билетов. Если требовать расставить знаки во всех 5 местах, то решений нет у 283730 билетов.


        1. igormu
          10.10.2017 08:35

          del


  1. phvoryh
    09.10.2017 19:29
    +1

    Год назад на хабре уже заходила речь об этой игре, тогда я подсчитал общее количество счастливых таким образом билетов, а заодно попытался сгенерировать наиболее сложные билеты. Если кому-то хочется поломать голову, привожу их список по возрастанию сложности (по моему субъективному мнению). Но в известной мне версии игры необязательно ставить знаки между всеми цифрами, то есть для билета 777777 допустимым решением является (777-77)/7, в примерах это используется.

    101048
    (10+10/4)*8


    1. ltvlx Автор
      09.10.2017 19:32

      Классно! Подставил все числа в программу, у каждой оказалось единственное решение.


      1. VovanZ
        09.10.2017 20:17

        А есть ли у дьявольского числа 666666 другие решения, кроме ((666 - 66) / 6) = 100?
        Моя наколенная поделка больше не находит.


      1. hacklex
        10.10.2017 19:43

        нет, у 146778 есть ((1+4*6)/(7+7))*8

        Программа точно хорошо работает с дробями?


        1. ltvlx Автор
          11.10.2017 00:02

          Вы где-то ошиблись.


          1. hacklex
            11.10.2017 15:09

            да, Вы правы, в «7+7==2» )))


    1. Deosis
      10.10.2017 07:44

      Недавно видел задачу: построить наилучшее приближение числа e, используя все цифры ровно по одному разу. Можно использовать любые операции.


      1. phvoryh
        10.10.2017 10:08
        +2

        Насколько любые?
        Можно написать 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.


  1. VovanZ
    09.10.2017 20:15

    Набросал на коленке очень медленное и весьма говнокодерское решение на питоне. Посмотрим, как быстро pythonanywhere накажет меня за хаброэффект :)
    Потыкать онлайн
    gist


    1. mikhaelkh
      10.10.2017 04:20

      Для числа 146778 не находит решения (14-6/7)*7+8, видимо по-хорошему надо написать класс дроби и считать в нём.


      1. VovanZ
        10.10.2017 11:28

        Я и так использую встроенный в питон класс дроби


        Оно находит не все решения, т. к. там где-то есть баг в переборе возможных способов расставления скобок.


        Но мне уже лень искать и исправлять этот баг :)


  1. andy_p
    09.10.2017 21:43

    Все уже придумано давным-давно:

    andyplekhanov.narod.ru/science/rnumber.htm


  1. wataru
    10.10.2017 10:57

    Оценку комбинаций можно сократить, если считать склейку цифр пятой операцией. Тогда вместо 2^N ? 4^N ? N! = 8^N ? N!, получается 5^N ? N!.. Все-равно завышено, ибо приоритет у склейки всегда будет максимальный, но это уже всего 375000 комбинаций.


    Интересно, нашли ли вы хоть одно число, для которого решения нет?


    1. ltvlx Автор
      10.10.2017 15:25

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

      Пример чисел без решений привели выше.