Resolviendo un rompecabezas de búsqueda universitaria usando Python / Sudo Null IT News

En blanco y negro – uno de los acertijos interesantes del juego Búsqueda de rompecabezas Universidad de Melbourne 2010. En la trama del juego, persigues a un misterioso participante de un programa de televisión con la esperanza de revelar su identidad. Consigues entrar primero al estudio y luego a su camerino. Allí, entre su ropa, encuentras un trozo de papel. Uno de sus lados está ocupado por un mensaje, el otro por un rompecabezas y un conjunto de instrucciones para ello.

“Organiza cada uno de los cuadros a continuación en tiras de 1×1, 1×2 o 1×3 para que ninguna cuadrícula contenga tiras con el mismo patrón en blanco y negro, incluidas las rotaciones”.

El rompecabezas consta de 25 campos cuadrados dispuestos en 5 filas y 5 columnas. A su vez, cada campo se divide en 25 celdas mediante líneas horizontales y verticales. Las celdas tienen un fondo blanco o negro y cada una de ellas contiene una letra.

Primero, determinemos qué tiras podemos utilizar para solucionar este problema. Hay 6 tiras diferentes de 1×3 (WWW, WWB, WBW, WBB, BWB y BBB), 3 tiras diferentes de 1×2 (WW, WB y BB) y 2 tiras diferentes de 1×1 (W y B). En total, todas estas tiras contienen 26 celdas. Esto significa que para descomponer cada campo tendrás que utilizar todas las tiras de 1×3, todas las tiras de 1×2 y solo una de las tiras de 1×1. Dado que la ubicación de la tira de 1 celda se deriva de la ubicación de las 9 tiras restantes, se puede ignorar durante la descomposición. Por lo tanto, el problema se puede reformular de la siguiente manera: es necesario colocar en cada campo 6 tiras únicas de tamaño 1×3 y 3 tiras únicas de tamaño 1×2 para que los colores de las celdas del campo y los colores de las celdas de las rayas coincidir. Intentemos resolver este problema usando Python.

Representemos franjas usando una tupla de líneas en orden aleatorio, donde los caracteres de cada línea indicarán los colores de las celdas correspondientes de la franja ('w' o 'b').

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

Imaginemos cada campo como una tupla de cadenas, cada una de las cuales representará una fila del campo. En este caso, los símbolos de las filas representarán los colores de las celdas correspondientes.

# 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')

También reuniremos todos los campos en una tupla.

# 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)

Para mayor claridad, convertiremos la solución de cada campo en un esquema simple que consta de barras verticales, guiones bajos y espacios. Además, durante la salida colocaremos varios diagramas en un nivel horizontal para que el resultado corresponda a la ubicación de los campos en la tarea. Proporcionemos un parámetro que indicará la cantidad de soluciones en un nivel durante la salida y lo estableceremos en cinco.

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

El programa comenzará con la función 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

Sus datos de entrada son: una tupla de campos, una tupla de franjas y también un parámetro de salida. Para descomponer cada campo, el programa irá colocando rayas en él de acuerdo con su orden en la tupla; Por tanto, antes de iniciar la búsqueda, tiene sentido ordenar las tiras según su longitud en orden descendente. Como resultado, la búsqueda considerará menos opciones potenciales en el camino hacia una solución, lo que simplificará enormemente la tarea. Para la búsqueda será conveniente representar cada campo como un diccionario, en el que las claves serán tuplas de dos números que indican el número de columna y el número de fila de la celda, y los valores serán cadenas de un carácter que indican el color. de la celda dada ('w' o 'b'). Para cada campo de la tupla, la función genera dicha representación y luego ejecuta una búsqueda en profundidad con ella. La solución encontrada luego se convierte en un circuito que se agregará a la tupla general. Una vez completado este proceso, la función organiza los diagramas en niveles y devuelve el resultado.

Veamos ahora la función de búsqueda en profundidad.

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

Esta función primero verifica si la tupla de franjas está vacía. Si este es el caso, entonces ya se han colocado todas las rayas, y solo queda devolver el arreglo resultante. De lo contrario, la función intenta colocar la primera franja de la tupla en el campo y luego, si tiene éxito, pasa la tupla de las franjas restantes a la siguiente llamada. Las posibles posiciones de la tira se generan utilizando la función get_strip_positions. Esta función devuelve un generador que proporcionará posiciones según sea necesario; cada uno de ellos se representará de dos formas diferentes mediante una tupla de dos elementos. La primera representación se utiliza para buscar: es un diccionario, donde las claves son tuplas de dos números, que indican la ubicación de las celdas de la franja en el campo, y los valores son cadenas de un carácter, que indican los colores de la franja. celdas ('w' o 'b'). La segunda representación se utilizará para generar una respuesta al problema, ya que es más conveniente para la derivación posterior de la solución. Esta representación es una tupla de 3 elementos: el primer elemento denota la celda inferior izquierda de la tira en una posición determinada como una tupla de 2 números; el segundo elemento representa la orientación de la franja como una línea (“horizontal” o “vertical”); el tercer elemento indica la longitud de la tira como un número. La función utiliza la primera representación para comprobar que los colores de las celdas de la franja coincidan con los colores de las celdas del campo en una posición determinada, y también para asegurarse de que las celdas correspondientes del campo no estén ocupadas por otras franjas. Si la posición es posible, entonces la función completa la tupla de celdas ocupadas del campo, agrega una segunda representación de la franja a la solución y continúa la búsqueda con nuevos datos.

Veamos ahora la función 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))

En primer lugar, “despliega” la tira 180 grados y comprueba si el resultado coincide con la versión original. Si este no es el caso, entonces se deben considerar más a fondo ambas opciones de ubicación. Primero, la función genera todas las posiciones horizontales posibles de la franja, considerando las líneas de campo por turno, luego las verticales, mirando por turno las columnas de campo.

Una vez que se encuentra una solución, la función solve_grids la pasa a la función get_placing_outline para generar el diseño de la tira para la salida.

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

Primero, esta función extrae de la solución todas las celdas del campo cuyo lado izquierdo no es el límite de ninguna franja, y todas las celdas del campo cuyo lado inferior no es el límite de ninguna franja. Luego, la función utiliza barras horizontales, guiones bajos y espacios para crear un diagrama de solución como una lista de cadenas, cada una de las cuales representa un nivel del diagrama. Debe considerar una fila adicional para mostrar el borde superior del campo y una columna adicional para mostrar el borde derecho del campo.

El esquema de cada solución se agrega a una tupla común, que luego se pasa a la función get_output, que genera el resultado final.

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

Esta función combina los mapas de decisión para cada nivel de salida horizontal en una lista. Inicialmente, cada nivel de salida es simplemente el primer circuito de ese nivel. Los circuitos posteriores se agregan a la lista nivel por nivel, seguidos de un espacio. Una vez que se genera el nivel de salida, se convierte en una cadena con nuevas líneas agregadas.

Ahora solo queda agregar comandos al programa para descomponer los campos indicados en la tarea y mostrar esquemas para su solución.

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

El resultado del programa se puede ver a continuación.

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

Los esquemas de solución resultantes ahora deben superponerse a las imágenes originales.

Se debe prestar atención a las franjas de una celda para cada solución. Las letras de estas celdas, en conjunto, suman el mensaje: “UNA CUADRÍCULA MÁS. USAR TIRAS NEGRAS” (OTRO CAMPO. USAR TIRAS NEGRAS). En este caso, cada franja de 1 celda ocupa una posición única. Así, a partir de estas celdas podrás crear otro campo de 5×5 sin espacios ni superposiciones.

Creemos una representación de este campo.

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

Agreguemos comandos al programa para descomponerlo y generar un diagrama de solución.

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

El resultado de su ejecución se puede ver a continuación.

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

A continuación, superpongamos el diagrama a la imagen original (el borde izquierdo de la celda con la letra T es incorrecto, las letras B y L deben intercambiarse).

Según la sugerencia, debes prestar atención a las rayas que consisten únicamente en celdas negras. Las letras dentro de estas franjas forman la palabra DOMINO. Es respuesta en asignación.

El código completo del programa se puede encontrar Aquí.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *