Многие задачи можно решить красиво и изящно, с помощью логики и математики. И почти все задачи можно решить в лоб — простым перебором вариантов. Компьютер с этим справляется гораздо быстрее человека, поэтому мы уже решили так несколько задач:
задача про наноботов → решили кодом
задача про безумного рекрутера → решили кодом
угадывание числа за 7 попыток → тоже написали код
Всё это были простые задачки, которые при желании можно было решить перебором и без компьютера. А сегодня мы обрушим всю компьютерную мощь на задачу Эйнштейна. Её сложность в том, что у неё 24,8 млрд возможных комбинаций — перебрать такое на листочке точно не получится.
Наша задача звучит так:
- На улице стоят пять домов.
 - Англичанин живёт в красном доме.
 - У испанца есть собака.
 - В зелёном доме пьют кофе.
 - Украинец пьёт чай.
 - Зелёный дом стоит сразу справа от белого дома.
 - Тот, кто майнит Bitcoin, разводит улиток.
 - В жёлтом доме майнят Ethereum.
 - В центральном доме пьют молоко.
 - Норвежец живёт в первом доме.
 - Сосед того, кто майнит Stellar, держит лису.
 - В доме по соседству с тем, в котором держат лошадь, майнят Ethereum.
 - Тот, кто майнит IOTA, пьёт апельсиновый сок.
 - Японец майнит Monero.
 - Норвежец живёт рядом с синим домом.
 
В целях ясности следует добавить, что каждый из пяти домов окрашен в свой цвет, а их жители — разных национальностей, владеют разными животными, пьют разные напитки и майнят разные криптовалюты. Ещё одно замечание: в утверждении 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 часа — это надёжно, но очень долго. Мы решили задачу в лоб, просто проверяя все комбинации подряд. Но на самом деле работу алгоритма можно ускорить во много раз — просто за счёт оптимизации. Этим и займёмся в следующий раз.
						

