easy

Решаем задачу Эйнштейна перебором (и программированием)

Проверим 24 миллиарда комбинаций, почти не задействуя мозг

Многие задачи можно решить красиво и изящно, с помощью логики и математики. И почти все задачи можно решить в лоб — простым перебором вариантов. Компьютер с этим справляется гораздо быстрее человека, поэтому мы уже решили так несколько задач:

задача про наноботоврешили кодом

задача про безумного рекрутера → решили кодом

угадывание числа за 7 попытоктоже написали код

Всё это были простые задачки, которые при желании можно было решить перебором и без компьютера. А сегодня мы обрушим всю компьютерную мощь на задачу Эйнштейна. Её сложность в том, что у неё 24,8 млрд возможных комбинаций — перебрать такое на листочке точно не получится.

Наша задача звучит так:

  1. На улице стоят пять домов.
  2. Англичанин живёт в красном доме.
  3. У испанца есть собака.
  4. В зелёном доме пьют кофе.
  5. Украинец пьёт чай.
  6. Зелёный дом стоит сразу справа от белого дома.
  7. Тот, кто майнит Bitcoin, разводит улиток.
  8. В жёлтом доме майнят Ethereum.
  9. В центральном доме пьют молоко.
  10. Норвежец живёт в первом доме.
  11. Сосед того, кто майнит Stellar, держит лису.
  12. В доме по соседству с тем, в котором держат лошадь, майнят Ethereum.
  13. Тот, кто майнит IOTA, пьёт апельсиновый сок.
  14. Японец майнит Monero.
  15. Норвежец живёт рядом с синим домом.

В целях ясности следует добавить, что каждый из пяти домов окрашен в свой цвет, а их жители — разных национальностей, владеют разными животными, пьют разные напитки и майнят разные криптовалюты. Ещё одно замечание: в утверждении 6 «справа» означает справа относительно вас.

Вопрос: кто пьёт воду, а у кого дома зебра?

Чтобы не было спорных моментов, добавим следующее:

  • дома расположены в ряд, друг за другом;
  • один из жильцов точно пьёт воду, и кто-то из жильцов точно держит дома зебру.

Логика решения кодом

Так как мы будем перебирать все варианты, нам нужно сначала их подготовить. У нас есть такие списки:

  • 5 национальностей,
  • 5 цветов дома,
  • 5 животных,
  • 5 напитков,
  • 5 криптовалют.

При проверке мы будем использовать такой принцип:

номер элемента в списке соответствует номеру дома

Это значит, что если на первом месте в списке национальностей стоит англичанин, а в списке цветов на том же месте — жёлтый, то это значит, что англичанин живёт в жёлтом доме. Точно так же будем работать и с остальными списками.

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

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

Из пяти элементов списка можно составить 120 различных вариантов перестановок. Это значит, что у нас будет 120 вариантов комбинаций национальностей, 120 вариантов комбинаций цветов дома и так далее. Нам нужно проверить все комбинации всех пяти списков, а это значит, что мы сделаем пять вложенных циклов. В итоге общее число комбинаций равно 120⁵ = 24 883 200 000.

Вариантов перебора у нас очень много, поэтому писать код будем на Python — с таким количеством вариантов он будет работать быстрее, чем на JavaScript.

👉 Как установить и начать писать на Python

Готовим данные

Пройдёмся по условию и запишем все найденные параметры каждый в свой список:

# исходные данные
nation_orig = ['eng','isp','ukr','nor','jap']
color_orig = ['red','green','white','yellow','blue']
animal_orig = ['dog','horse','snail','fox','zebra']
drink_orig = ['coffee','tea','milk','juice','water']
money_orig = ['bitcoin','etherium','stellar','iota','monero']

Теперь для каждого списка нужно составить все варианты перестановок — те самые 120 вариантов каждого списка. Используем для этого встроенный модуль itertools — с его помощью можно получить все перестановки одной командой:

itertools.permutations()

Permutations работает так: если у нас есть набор чего-то типа (1, 2, 3), то permutations вернёт нам (1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1).

Проблема в том, что нам нужны списки, а результат будет в виде кортежа. Нам будет удобнее работать с кортежами, поэтому сразу переведём результат в нужный вид командой list():

# модуль для быстрого получения всех перестановок
import itertools

# получаем все перестановки всех списков
nation_perm = list(itertools.permutations(nation_orig))
color_perm = list(itertools.permutations(color_orig))
animal_perm = list(itertools.permutations(animal_orig))
drink_perm = list(itertools.permutations(drink_orig))
money_perm = list(itertools.permutations(money_orig))

Проверяем условия

Нам нужно проверить сразу 15 условий для каждой комбинации. Чтобы было проще, вынесем проверку в отдельную функцию. На вход она получит текущие варианты всех списков, поэтому сразу укажем их как входные параметры:

# выполняем все проверки, заданные в условии
def all_check(nation, color, animal, drink, money):

Разберём все проверки на трёх условиях, а остальные сделаем по аналогии.

Англичанин живёт в красном доме. Мы договорились, что индексы элементов у нас отвечают за номера домов. Для проверки первого условия нам нужно посмотреть, совпадает ли индекс национальности с индексом цвета. Для этого используем команду .index() — она вернёт индекс элемента в списке. В итоге проверка этого условия на Python будет выглядеть так:

if nation.index('eng') == color.index('red'):

В центральном доме пьют молоко. Элементы списка нумеруются с нуля: 0,1,2,3,4. Получается, средний дом — это дом с индексом 2. Проверим, стоит ли молоко на этой позиции:

if drink.index('milk') == 2:

Сосед того, кто майнит Stellar, держит лису. Сосед — это значит, что индекс его дома на единицу больше или меньше соседнего. При этом нам неважно, с какой стороны сосед — слева или справа, нас устроит любой вариант. Получается, что мы можем выбрать или один вариант, или другой. Зная это, составим такую проверку:

(money.index('stellar') == animal.index('fox') - 1 or money.index('stellar') == animal.index('fox') + 1)

Теперь используем эти принципы, чтобы составить все 14 проверок из условия задачи. В конце сразу добавим вывод найденной комбинации, если все проверки пройдут успешно:

# выполняем все проверки, заданные в условии
def all_check(nation, color, animal, drink, money):
    # проверяем все условия сразу
    if (nation.index('eng') == color.index('red') 
        and nation.index('isp') == animal.index('dog')
        and color.index('green') == drink.index('coffee')
        and nation.index('ukr') == drink.index('tea')
        and color.index('green') - 1 == color.index('white')
        and money.index('bitcoin') == animal.index('snail')
        and color.index('yellow') == money.index('etherium')
        and drink.index('milk') == 2
        and nation.index('nor') == 0
        and (money.index('stellar') == animal.index('fox') - 1 or money.index('stellar') == animal.index('fox') + 1)
        and (animal.index('horse') == money.index('etherium') - 1 or animal.index('horse') == money.index('etherium') + 1)
        and money.index('iota') == drink.index('juice')
        and nation.index('jap') == money.index('monero')
        and (nation.index('nor') == color.index('blue') - 1 or nation.index('nor') == color.index('blue') + 1)):
        # выводим найденный результат    
        print('✅✅✅')
        print(color)
        print(nation)
        print(drink)
        print(money)
        print(animal)

Организуем циклы

Здесь всё просто: нам нужно сделать пять вложенных циклов, где в каждом мы будем перебирать свои характеристики — национальность, цвет дома и так далее. Так как всего перестановок в каждой характеристике у нас 120, то и каждый цикл мы тоже сделаем в этом диапазоне.

Чтобы знать, что программа не зависла и работает, добавим внутри первого вывод текущего номера цикла — так мы увидим, на какой стадии сейчас выполняются проверки.

В последнем вложенном цикле нам нужно передать в функцию каждую комбинацию именно в виде списка. Для этого снова используем команду list(), а в качестве индекса будем брать текущие переменные цикла:

# перебираем все комбинации по очереди
for n in range(120):
    print(n)
    for c in range(120):
        for a in range(120):
            for d in range(120): 
                for m in range(120):
                    # и проверяем, выполнились ли все условия задачи
                    all_check(list(nation_perm[n]), list(color_perm[c]), list(animal_perm[a]), list(drink_perm[d]), list(money_perm[m]))

Запускаем код

После запуска мы увидим, что числа, отвечающие за ход первого цикла, появляются небыстро — примерно раз в 2 минуты. Это значит, что на перебор и проверку всех 120 вариантов у нас уйдёт примерно 4 часа. Чтобы не ждать впустую столько времени, запустим код и пойдём спать.

Когда программа закончила работать, посмотрим вывод в терминале — появилось там решение или нет:

Решаем задачу Эйнштейна перебором (и программированием)
Где-то в ночи получили решение задачи

Если мы сравним найденное решение с тем, что мы получили с помощью выкладок, то увидим, что они полностью совпадают:

Решаем задачу Эйнштейна перебором (и программированием)
Здесь у нас нет зебры — она пойдёт в единственную свободную клетку в углу

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

Что дальше

Перебор за 4 часа — это надёжно, но очень долго. Мы решили задачу в лоб, просто проверяя все комбинации подряд. Но на самом деле работу алгоритма можно ускорить во много раз — просто за счёт оптимизации. Этим и займёмся в следующий раз.

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
easy