Решение головоломки Black and White.

Black and White - одна из интересных головоломок игры Puzzle Hunt Мельбурнского Университета 2010 года. По сюжету игры вы преследуете загадочного участника ТВ-шоу в надежде раскрыть его личность. Вам удается пробраться сначала на студию, а затем и в его гримерку. Там в его одежде вы находите клочок бумаги. Одну из его сторон занимает сообщение, другую - головоломка и набор инструкций к ней. 

«Разложите каждую из диаграмм ниже на полоски размером 1x1, 1x2 или 1x3 таким образом, чтобы ни одна сетка не содержала полосок с одинаковым черно-белым паттерном, включая повороты».

Головоломка представляет собой 25 квадратных полей, расположенных в 5 рядов и 5 столбцов. В свою очередь, каждое поле разделено на 25 клеток горизонтальными и вертикальными линиями. Клетки имеют белый или черный фон, и каждая из них содержит по одной букве. 

Сначала определим, какие полоски мы можем использовать для решения этой задачи. Существует 6 различных полосок размером 1х3 (WWW, WWB, WBW, WBB, BWB и BBB), 3 различных полоски размером 1х2 (WW, WB и BB) и 2 различных полоски размером 1х1 (W и B). В сумме все эти полоски содержат 26 клеток. Это означает, что для разложения каждого поля придется использовать все полоски размером 1х3, все полоски размером 1х2 и только одну из полосок размером 1х1. Так как расположение полоски из 1 клетки вытекает из расположения остальных 9 полосок, то ее можно не учитывать при разложении. Стало быть, задачу можно переформулировать следующим образом: необходимо расположить на каждом поле 6 уникальных полосок размером 1х3 и 3 уникальных полоски размером 1х2 таким образом, чтобы цвета клеток поля и цвета клеток полосок совпадали. Попробуем решить эту задачу с помощью Python.

Представим полоски с помощью кортежа из строк в произвольном порядке, где символы каждой строки будут обозначать цвета соответствующих клеток полоски ('w' или 'b').

# strips for the puzzle            
strips = ('ww','wb','bb','www','wwb','wbw','wbb','bwb','bbb')

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

# grids for the puzzle
grid01 = ('bwbww','bwbbb','wbwbw','bwwbw','bwwbb')
grid02 = ('bwbwb','bwbwb','wbwbw','wwbbb','wbbww')
grid03 = ('wwwbw','bbwww','bbbww','wwbbw','bbwbb')
grid04 = ('wwbbw','wbwbb','bwwwb','wwbbw','bbbwb')
grid05 = ('wwwwb','bbbbw','bbwbb','bwwbb','wwwwb')
grid06 = ('wbwwb','bwwbw','bbbbb','wwwbw','bwbww')
grid07 = ('wbwww','wwbbw','wbbbw','bbbbw','wbbww')
grid08 = ('wbbww','wwwbb','bwbww','bwbwb','bbwwb')
grid09 = ('bbbww','wwbww','wbbww','bwwwb','bbwbb')
grid10 = ('wwbbb','wbbbb','wbbwb','bwbww','bwwww')
grid11 = ('wwwww','bbbbb','wwbbw','wwbbb','bbbww')
grid12 = ('bbbbb','wwbwb','wwwwb','wwbwb','wwbwb')
grid13 = ('bwbwb','wwwbb','bwbwb','bwbbw','wwbwb')
grid14 = ('wbwwb','wbwbb','wwbbb','wwbbb','wwbbw')
grid15 = ('wwbbw','wwbww','bbbww','bbbww','bbwbw')
grid16 = ('wbwbw','wbwww','bbbbb','bwwww','bwbwb')
grid17 = ('wwwwb','wwwww','bbbbw','bbbwb','bwbbb')
grid18 = ('wbbww','bwwbb','bwwwb','bbbwb','wbwwb')
grid19 = ('bwwbw','wbwww','bwbwb','bwwbw','bbbbw')
grid20 = ('wbbwb','bbwbw','wwwwb','wbbbb','wwbwb')
grid21 = ('bbbwb','bbwbw','wbbww','bbwwb','wwbww')
grid22 = ('wwbbw','wbbbw','bwwwb','bwbbb','bwbww')
grid23 = ('wbwww','wwbwb','bbwww','wbwbb','bbbwb')
grid24 = ('bwwbb','wwwww','bwwww','bbbbb','wwbbb')
grid25 = ('wwwww','bbbww','bbbbw','bwbww','wwbbb')

Все поля также соберем вместе в один кортеж.

# given grids together as a tuple
grids = (grid01,grid02,grid03,grid04,grid05,
         grid06,grid07,grid08,grid09,grid10,
         grid11,grid12,grid13,grid14,grid15,
         grid16,grid17,grid18,grid19,grid20,
         grid21,grid22,grid23,grid24,grid25)

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

# number of outlines of placings in one row for the output
outlines_per_row = 5

Работа программы будет начинаться с функции solve_grids. 

def solve_grids(grids, strips, outlines_per_row):
    """Function for solving grids for puzzle Black and White
       from MUMS Puzzle Hunt 2010 competition."""
    first_grid = grids[0]
    num_rows = len(first_grid)
    num_cols = len(first_grid[0])
    # sorting strips according to length in decreasing order
    # to make the problem easier
    strips = tuple(sorted(strips,key = len,reverse = True))
    outlines = ()
    for grid_values in grids:
        # represent the grid as a dictionary for search
        grid = {}
        for row in range(num_rows):
            for col in range(num_cols):
                grid[(col,row)] = grid_values[row][col]
        # try to find placing with depth-first search
        placing = depth_first_search(grid, num_cols, num_rows, strips,(), ())
        if placing:
            # form outline of a placing
            outline = get_placing_outline(placing, num_cols, num_rows)
            outlines += (outline,)
        else:
            return False
    # combine outlines
    output = get_output(outlines, outlines_per_row)
    return output

Ее входными данными являются: кортеж полей, кортеж полосок, а также параметр для вывода. Для разложения каждого поля программа будет по очереди располагать на нем полоски в соответствии с их порядком в кортеже; поэтому перед началом поиска имеет смысл отсортировать полоски в соответствии с их длинной в убывающем порядке. В результате этого поиск будет рассматривать меньше потенциальных вариантов на пути к решению, что значительно упростит задачу. Для поиска будет удобно представить каждое поле в виде словаря, в котором ключами будут кортежи из двух чисел, обозначающие номер столбца и номер строки клетки, а значениями - строки из одного символа, обозначающие цвет данной клетки ('w' или 'b'). Для каждого поля из кортежа функция формирует такое представление, а затем запускает с ним поиск в глубину. Найденное решение затем преобразуется в схему, которая будет добавляться в общий кортеж. После завершения этого процесса функция располагает схемы по уровням и возвращает результат.

Рассмотрим теперь функцию поиска в глубину. 

def depth_first_search(grid, num_cols, num_rows, strips, occupied_cells, placing):
    """Function that perform depth-first search to place the strips on the grid."""
    if strips == ():
        # all strips are placed
        return placing
    else:
        # current strip of search
        current_strip = strips[0]
        # strips for further search
        next_strips = strips[1:]
        # position is used for search, representation is used for answer
        for (position,representation) in get_strip_positions(current_strip, num_cols, num_rows):
            position_is_possible = True
            # check that position is possible
            for cell in position:
                if position[cell] != grid[cell] or cell in occupied_cells:
                    position_is_possible = False
                    break
            if position_is_possible:
                next_occupied_cells = occupied_cells                        
                for cell in position:
                    next_occupied_cells += (cell,)
                next_placing = placing + (representation,)
                final_placing = depth_first_search(grid, num_cols, num_rows, next_strips, next_occupied_cells, next_placing)
                if final_placing:
                    return final_placing
    return False

Сначала эта функция проверяет, не является ли кортеж полосок пустым. Если это так, то все полоски уже размещены, и остается вернуть получившееся расположение. В противном случае функция пытается разместить на поле первую полоску из кортежа, а затем, при удачном исходе, передает кортеж из оставшихся полосок в следующий вызов. Возможные позиции полоски генерируются с помощью функции get_strip_positions. Эта функция возвращает генератор, который будет предоставлять позиции по мере необходимости; при этом каждая из них будет представлена двумя различными способами с помощью кортежа из двух элементов. Первое представление используется для поиска: это словарь, где ключами служат кортежи из  двух чисел, обозначающие расположение клеток полоски на поле, а значениями - строки из одного символа, обозначающие цвета клеток полоски ('w' или 'b'). Второе представление будет использоваться, чтобы сформировать ответ на задачу, так как оно более удобно для последующего вывода решения. Это представление является кортежем из 3 элементов: первый элемент обозначает левую нижнюю клетку полоски при данной позиции в виде кортежа из 2 чисел; второй элемент представляет ориентацию полоски в виде строки ('horizontal' или 'vertical'); третий элемент обозначает длину полоски в виде числа. Функция использует первое представление, чтобы проверить, что цвета клеток полоски совпадают с цветами клеток поля при данной позиции, а также удостовериться, что соответствующие клетки поля не заняты другими полосками. Если позиция возможна, то функция дополняет кортеж занятых клеток поля, добавляет второе представление полоски к решению и продолжает поиск уже с новыми данными.

Рассмотрим теперь функцию get_strip_positions. 

def get_strip_positions(strip, num_cols, num_rows):
    """Function that generate possible positions for the given strip
       according to the number of columns and rows in the grid."""
    strip_len = len(strip)
    # we should also consider reversed strip,
    # if it is different from the original one
    reversed_strip = strip[::-1]
    if strip == reversed_strip:
        patterns = (strip,)
    else:
        patterns = (strip, reversed_strip)
    # generate horizontal placings of the strip 
    for row in range(num_rows):
        for col in range(num_cols - strip_len + 1):
            for pattern in patterns:
                position = {}
                current_col = col
                for colour in pattern:
                    position[(current_col, row)] = colour
                    current_col += 1
                yield (position, ((col,row),'horizontal',strip_len))
    # generate vertical placings of the strip 
    for col in range(num_cols):
        for row in range(num_rows - strip_len + 1):
            for pattern in patterns:
                position = {}
                current_row = row
                for colour in pattern:
                    position[(col, current_row)] = colour
                    current_row += 1
                yield (position, ((col,row),'vertical',strip_len))

Первым делом она «разворачивает» полоску на 180 градусов и проверяет, совпадает ли результат с первоначальным вариантом. Если это не так, то дальше следует рассматривать оба варианта расположения. Сначала функция формирует все возможные горизонтальные позиции полоски, рассматривая по очереди строки поля, затем - вертикальные, рассматривая по очереди столбцы поля.

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

def get_placing_outline(placing, num_cols, num_rows):
    """Function that creates outline of the placing for output
       that consists of bars, underscores and spaces."""
    cells_without_left_border = ()
    cells_without_lower_border = ()
    for strip in placing:
        first_cell = strip[0]
        col,row = first_cell[0],first_cell[1]
        orientation = strip[1]
        strip_len = strip[2]
        if orientation == 'horizontal':
            for strip_index in range(1, strip_len):
                cells_without_left_border += ((col + strip_index, row),)
        elif orientation == 'vertical':
            for strip_index in range(1, strip_len):
                cells_without_lower_border += ((col, row + strip_index),)
    outline = []
    # decremental loop for rows with one additional row for the upper border of the grid
    for row in range(num_rows,-1,-1):
        level = ''
        # loop for cols with one additional col for the right border of the grid
        for col in range(num_cols+1):
            cell = (col,row)
            if row < num_rows and (col == 0 or col == num_cols or not cell in cells_without_left_border):
                level += '|'
            else:
                level += ' '
            if col < num_cols:
                if row == 0 or row == num_rows or not cell in cells_without_lower_border:
                    level += '_'
                else:
                    level += ' '
        outline.append(level)
    return outline

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

Схема каждого решения добавляется в общий кортеж, который затем передается в функцию get_output, формирующую итоговой вывод. 

def get_output(outlines, outlines_per_row):
    """Function that combines outlines to create output
       with outlines_per_row outlines in one row."""
    outlines_len = len(outlines)
    output = ''
    # determine starting index for every row
    for first_index in range(0, outlines_len, outlines_per_row):
        last_index = min(first_index + outlines_per_row, outlines_len)
        # add first outline to the row
        one_row = outlines[first_index]
        # add other outlines to the row
        for current_outline in outlines[first_index+1:last_index]:
            level_index = 0
            for level in current_outline:
                one_row[level_index] += ' ' + level
                level_index += 1
        for level in one_row:
            output += level + '\n'
    return output

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

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

if __name__ == '__main__':
    # solve the given grids for the puzzle
    answer = solve_grids(grids, strips, outlines_per_row)
    print(answer)

Результат работы программы можно видеть ниже.

 _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _ 
| | | |_ _| | |_ _|_ _| | |_ _ _| | |_ _ _| | | |_ _ _|_ _|
| | | | | | | | |_ _ _| |_|_ _ _| | |_ _ _|_| | | |_ _ _|_|
|_|_|_| |_| |_| | | | | |_ _ _|_|_| |_|_ _ _|_| | | | | | |
|_ _ _|_| | |_|_|_| | | |_ _|_ _ _| |_ _ _|_ _| |_|_| | | |
|_|_ _ _|_| |_ _ _|_|_| |_ _|_ _ _| |_ _|_ _ _| |_ _|_|_|_|
 _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _ 
|_ _ _|_ _| | | |_ _ _| | | |_ _ _| | | |_ _ _| | |_ _ _| |
|_ _ _| | | | |_| |_ _| | |_|_ _ _| |_| |_ _ _| | |_ _ _|_|
| |_ _| | | |_| | | | | |_|_ _ _|_| | |_|_ _| | |_|_|_ _ _|
| | |_|_|_| | | |_| | | |_ _ _|_ _| |_|_ _ _| | |_ _|_ _ _|
|_|_|_ _ _| |_|_|_|_|_| |_ _ _|_ _| |_ _ _|_|_| |_ _ _|_ _|
 _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _ 
|_ _ _|_ _| | |_|_ _ _| |_ _ _|_ _| |_ _ _| | | | |_ _ _|_|
|_ _ _|_| | | |_ _ _| | | | |_ _ _| | | | | | | | | |_ _ _|
| |_ _ _| | |_| | | | | |_|_|_| | | | |_|_|_|_| |_|_|_ _| |
| | |_ _|_| | | | |_|_| |_ _ _| | | |_|_ _ _|_| | |_ _ _| |
|_|_|_ _ _| |_|_|_|_ _| |_ _ _|_|_| |_ _ _|_ _| |_|_ _ _|_|
 _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _ 
| |_ _ _| | |_ _ _|_| | |_ _ _|_ _| |_ _ _| | | |_ _|_ _ _|
|_| |_ _|_| |_ _ _| | | | |_|_ _ _| | | |_|_|_| | |_ _ _| |
| | |_ _ _| | |_ _| |_| | |_ _ _| | | | |_ _ _| | |_ _ _|_|
| |_|_ _ _| | |_ _|_| | |_|_ _ _|_| |_|_|_ _ _| |_|_|_ _ _|
|_|_ _ _|_| |_|_ _ _|_| |_ _ _|_ _| |_ _|_ _ _| |_ _ _|_ _|
 _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _   _ _ _ _ _ 
|_ _ _| | | | | |_| | | |_ _ _| | | | | |_ _ _| |_|_ _ _| |
|_ _| | |_| | | | | | | |_| | | | | |_| |_ _| | |_ _ _| | |
| | | |_| | |_|_| |_|_| | |_|_|_|_| | |_|_ _| | |_ _ _| |_|
| | |_|_|_| |_ _|_| | | | | |_ _ _| | |_ _ _|_| | |_ _|_| |
|_|_|_ _ _| |_ _ _|_|_| |_|_|_ _ _| |_|_|_ _ _| |_|_ _ _|_|

Полученные схемы решений теперь необходимо наложить на исходные изображения.

Внимание следует обратить на полоски из одной клетки для каждого решения. Буквы в этих клетках, взятые вместе, складываются в сообщение: "ONE MORE GRID. USE BLACK STRIPS" (ЕЩЕ ОДНО ПОЛЕ. ИСПОЛЬЗУЙТЕ ЧЕРНЫЕ ПОЛОСКИ). При этом каждая полоска из 1 клетки занимает уникальную позицию. Таким образом, из этих клеток можно без пробелов и наложений составить еще одно поле размером 5х5.

S U R A B
I C K D O
M I S E G
N S R T E
O P E R L

Создадим представление этого поля.

# final grid for the puzzle
final_grid = ('bwwww','bbwbb','bbbww','wbwbb','wwbbw')

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

if __name__ == '__main__':
    # solve the final grid for the puzzle
    answer = solve_grids((final_grid,), strips, outlines_per_row)
    print(answer)

Результат их выполнения можно видеть ниже.

 _ _ _ _ _ 
|_ _|_ _ _|
|_ _ _|_ _|
| |_|_ _ _|
| |_ _ _| |
|_|_ _ _|_|

Далее наложим схему на исходное изображение (левая граница у клетки с буквой T ошибочна, буквы B и L следует поменять местами).

В соответствии с подсказкой внимание необходимо обратить на полоски, состоящие только из черных клеток. Буквы внутри этих полосок складываются в слово DOMINO. Оно и является ответом на задание.

Код программы целиком можно найти здесь.

Comments

Popular posts from this blog

Cicada 3301 - solution.

Solution to the puzzle Sleuth.

Solution to the puzzle Cat Walk.