Help us understand the problem. What is going on with this article?

何百~何千通りも答えのある箱詰めパズルを解く(レベル2:ペントミノ)

前回は「何百~何千通りも答えのある箱詰めパズルを解く」ということで、レベル1のテトロミノを解くプログラムをPythonで実装しましたが、レベル2のペントミノを解くことはできませんでした。今回は、改めてペントミノに挑戦します!

私の自宅にあるのは、こういうやつです。

前回の復習も兼ねて

xy のケースを初期化するコードは次のようになります。前回は -1 で初期化しましたが、今回は事情があって(後ほど説明します)、 0 で初期化するように変更しました。

def generate(x, y): # 0 で初期化するよう変更
    return [[0 for _ in range(x)] for _ in range(y)]

同様に、新しいピースを得る次の関数でも、 「ピースで埋められていないマス」を意味する -1 の部分を 0 に置き換えました。

def get_new_piece(matrix, piece_size):
    piece = []

    for x in range(x_length):
        for y in range(y_length):
            if matrix[y][x] == 0: ### 変更箇所
                piece.append([x, y])
                break
        if len(piece) > 0:
            break

    for i in range(piece_size - 1):
        neighbors = get_rand_neighbor(piece, matrix)
        random.shuffle(neighbors)
        for x, y in neighbors:
            if [x, y] not in piece:
                piece.append([x, y])
                break

    return piece

同様に、隣接するマスを得る次の関数でも、 「ピースで埋められていないマス」を意味する -1 の部分を 0 に置き換えました。

def get_rand_neighbor(piece, matrix):
    neighbors = []
    for x, y in piece:
        for dx, dy in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
            if x - dx < 0 or x - dx >= len(matrix[0]):
                pass
            elif y - dy < 0 or y - dy >= len(matrix):
                pass
            elif matrix[y - dy][x - dx] == 0: ### 変更箇所
                neighbors.append([x - dx, y - dy])

    return neighbors

パズルを図示する関数は、そのままです。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def depict(matrix):
    plt.imshow(matrix, interpolation='nearest', cmap=plt.cm.gist_ncar)
    plt.xticks(np.arange(len(matrix[0])), range(len(matrix[0])), rotation=90)
    plt.yticks(np.arange(len(matrix)), range(len(matrix)))
    plt.tight_layout()
    plt.show()

同一構造を判定する関数の改良

今回の重要な変更点は、ピースの同一構造を判定する次の関数です。

テトロミノは解けるのにペントミノはどうして解けないのかな〜〜〜??

と思っていろいろ試行錯誤していましたが、改良前のこの関数ではテトロミノのピースが全て区別できるのに対し、ペントミノのピースの中に区別できないものがあるということが分かりました。

そりゃペントミノ解けんわな...

ということで、改良済みの関数はこちらです。

def shape_key(piece):
    distances = []
    adjacents = {} ### 今回新たに加えた特徴量
    for i in range(len(piece)):
        xy1 = piece[i]
        for j in range(len(piece)):
            if i < j:
                xy2 = piece[j]
                distance = (xy1[0] - xy2[0])**2 + (xy1[1] - xy2[1])**2
                distances.append(distance)

                ### 以下、今回新たに加えた特徴量
                if distance == 1:
                    if i not in adjacents.keys():
                        adjacents[i] = []
                    adjacents[i].append(j)
                    if j not in adjacents.keys():
                        adjacents[j] = []
                    adjacents[j].append(i)

    #return "".join(str(sorted(distances))) 前回の返り値

    ### 今回の返り値
    return (
        "".join(str(sorted(distances))) + "_" 
        + "".join(str(sorted([len(adjacents[k]) for k in adjacents.keys()]))))

テトロミノ(レベル1)を解きます

ペントミノ(レベル2)を解く前に、テトロミノ(レベル1)が解けることを確認します。変更箇所がもう1つあることに注意。ランダムに試行錯誤を繰り返すので、実行するたびに出力結果は変わります。

import random
x_length = 8
y_length = 5
piece_size = 4
same_piece_limit = 2
max_trial = 530000
best_score = x_length * y_length
best_matrix = generate(x_length, y_length)

for trial in range(max_trial):
    matrix = generate(x_length, y_length)
    count = {}
    piece_id = 1 ### 変更箇所
    piece = get_new_piece(matrix, piece_size)
    key = shape_key(piece)
    count[key] = 1

    while len(piece) == piece_size:
        for x, y in piece:
            matrix[y][x] = piece_id

        piece = get_new_piece(matrix, piece_size)
        key = shape_key(piece)
        if key not in count.keys():
            count[key] = 0
        count[key] += 1
        if count[key] > same_piece_limit:
            break

        piece_id += 1

    score = sum([sum([1 if n == 0 else 0 for n in mat]) for mat in matrix])

    if best_score >= score:
        best_score = score
        best_matrix = matrix
        #depict(best_matrix)

    if score == 0:
        print(trial, "th trial")
        break

depict(best_matrix)
best_matrix
71 th trial

noblock_puzzle4_11_1.png

[[1, 1, 1, 1, 7, 7, 7, 7],
 [2, 2, 4, 6, 6, 6, 9, 9],
 [2, 4, 4, 4, 8, 6, 9, 9],
 [2, 3, 3, 5, 8, 8, 10, 10],
 [3, 3, 5, 5, 5, 8, 10, 10]]

ペントミノ(レベル2)を解きます

変数の値を変更すれば、ペントミノ(レベル2)を解けるはずです。やってみましょう。

x_length = 10
y_length = 6
piece_size = 5
same_piece_limit = 1
max_trial = 530000
best_score = x_length * y_length
best_matrix = generate(x_length, y_length)

for trial in range(max_trial):
    matrix = generate(x_length, y_length)
    count = {}
    piece_id = 1 ###
    piece = get_new_piece(matrix, piece_size)
    key = shape_key(piece)
    count[key] = 1

    while len(piece) == piece_size:
        for x, y in piece:
            matrix[y][x] = piece_id

        piece = get_new_piece(matrix, piece_size)
        key = shape_key(piece)
        if key not in count.keys():
            count[key] = 0
        count[key] += 1
        if count[key] > same_piece_limit:
            break

        piece_id += 1

    score = sum([sum([1 if n == 0 else 0 for n in mat]) for mat in matrix])

    if best_score >= score:
        best_score = score
        best_matrix = matrix
        #depict(best_matrix)

    if score == 0:
        print(trial, "th trial")
        break

depict(best_matrix)
best_matrix

noblock_puzzle4_13_0.png

[[1, 1, 1, 5, 6, 6, 6, 6, 10, 0],
 [1, 1, 5, 5, 7, 7, 6, 10, 10, 11],
 [2, 2, 4, 5, 5, 7, 10, 10, 11, 11],
 [2, 4, 4, 4, 8, 7, 7, 9, 11, 0],
 [2, 2, 4, 3, 8, 9, 9, 9, 11, 0],
 [3, 3, 3, 3, 8, 8, 8, 9, 0, 0]]

うーむ、、、ペントミノ(レベル2)のピースは全て区別できるようになったはずだけど、530000回繰り返してもまだ解けないとは...。ほっほっほ、初めてですよ...ここまで私をコケにしたおバカさん達は…

探索方法を変更

上記の方法では、ランダムな探索を繰り返していました。このため、同じ場所の探索を何度も繰り返している可能性があります。

そこで、次のような改良を加えることで、いくつか無駄な探索を減らす努力をしてみました。

  • グラフ理論でいう「順位優先探索」を行なうことで、同じ場所の探索を二度と繰り返さないようにした。

  • グラフ理論でいう「連結成分」という概念を用いて、ピースに埋められていないマスの連結成分が2つ以上になるピースの置き方をした時は、それ以上の探索を打ち切ることにした。

  • これまでに置いたピースの集合と、ピースを置いていないマスの境界線を「表面」とし、新しく置くピースがその表面と大きく接するとき、そのピースを優先するようにした。

  • 全ピースの半分だけケースに収める計算ならすぐできるので、半分だけ収める計算を行い、その「半分だけ解いたパズル」の組み合わせの中から条件を満たす組み合わせを発見する方針に変更した。

次に置けるピースを全列挙する

次に置けるピースを全列挙する関数がこちらになります。これを用いて順位優先探索することになります。

import copy

def get_new_pieces(matrix):
    piece = []

    for x in range(x_length):
        for y in range(y_length):
            if matrix[y][x] == 0:
                piece.append([x, y])
                break
        if len(piece) > 0:
            break

    result_pieces = []
    waiting = []
    waiting.append(piece)
    while len(waiting) > 0:
        piece = waiting.pop()
        neighbors = get_rand_neighbor(piece, matrix)
        for x, y in neighbors:
            if [x, y] not in piece:
                new_piece = copy.deepcopy(piece)
                new_piece.append([x, y])
                if len(new_piece) == piece_size:
                    new_piece = sorted(new_piece)
                    if new_piece not in result_pieces:
                        result_pieces.append(new_piece)
                else:
                    waiting.append(new_piece)

    return sorted(result_pieces)

ピースを置いてない場所の連結成分

ピースに埋められていないマスの連結成分を得る関数はこちらです。

def get_connected_subgraphs(matrix):
    neigh = {}
    for y in range(len(matrix)):
        for x in range(len(matrix[y])):
            if matrix[y][x] == 0:
                neigh[",".join([str(x), str(y)])] = []
                for dx, dy in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
                    if x - dx < 0 or x - dx >= len(matrix[0]):
                        pass
                    elif y - dy < 0 or y - dy >= len(matrix):
                        pass
                    elif matrix[y - dy][x - dx] == 0:
                        neigh[",".join([str(x), str(y)])].append([x - dx, y - dy])

    connected_subgraphs = []
    found = []
    for k, v in neigh.items():
        if k in found:
            continue
        connected_subgraph = [k]
        waiting = list(v)
        found.append(k)
        while len(waiting):
            x, y = waiting.pop()
            n = ",".join([str(x), str(y)])
            if n in found:
                continue
            connected_subgraph.append(n)
            found.append(n)
            if n in neigh.keys():
                waiting += neigh[n]
        connected_subgraphs.append(connected_subgraph)

    return connected_subgraphs

「表面」の大きさを決める関数

新しいピースを置いた時、既存のピースの集合と接する「表面」の大きさを interface とし、これを順位優先探索に用いることにしました。(surface も計算しますが、これは結局使いません)

def get_face(piece, matrix): 
    interface = 0
    surface = 0
    for x, y in piece:
        for dx, dy in [[0, 1], [0, -1], [1, 0], [-1, 0]]:
            if x - dx < 0 or x - dx >= len(matrix[0]):
                pass
            elif y - dy < 0 or y - dy >= len(matrix):
                pass
            elif matrix[y - dy][x - dx] == 0:
                surface += 1
            else:
                interface += 1
    return interface, surface

半分だけパズルを解く

今回の探索方針変更の大きなところです。全ピースを最後までケースに置こうとする方針では、テトロミノ(レベル1)は解けますがペントミノ(レベル2)は解けませんでした。そこで、最後まで埋めようとせず、全ピースの半分だけケースに置くことを繰り返し、その「半分だけ解いたパズル」を組み合わせて、いい組み合わせを探すことをします。

そのうち「半分だけ解いたパズル」を作る関数がこちらです。

また、基本的には「順位優先探索」ですが、ある低い確率で順位をランダムにする処理を加えています。

import random
def half_puzzle(x_length, y_length, piece_size, same_piece_limit):
    best_score = x_length * y_length
    best_matrix = generate(x_length, y_length)
    n_depict = 0
    n_pieces = int(x_length * y_length / piece_size)
    waiting = []
    piece_id = 1
    matrix = generate(x_length, y_length)
    for new_piece in get_new_pieces(matrix):
        pieces2count = {}
        key = shape_key(new_piece)
        pieces2count[key] = 1
        new_matrix = copy.deepcopy(matrix)
        for x, y in new_piece:
            new_matrix[y][x] = piece_id
        pieces = [new_piece]
        waiting.append([0, piece_id + 1, pieces, new_matrix, pieces2count])

    trial = 0
    random.shuffle(waiting)
    while len(waiting) > 0:
        trial += 1
        if trial > 530000:
            break

        delta, piece_id, pieces, matrix, pieces2count = waiting.pop()
        score = sum([sum([1 if x == 0 else 0 for x in mat]) for mat in matrix])
        if len(get_connected_subgraphs(matrix)) > 1:
            continue

        if best_score >= score:
            best_score = score
            best_matrix = matrix

        if score == (x_length * y_length) / 2:
            yield(best_matrix)
            continue

        new_pieces = get_new_pieces(matrix)
        for new_piece in new_pieces:
            new_pieces2count = copy.deepcopy(pieces2count)
            key = shape_key(new_piece)
            if key not in new_pieces2count.keys():
                new_pieces2count[key] = 0
            new_pieces2count[key] += 1
            if new_pieces2count[key] > same_piece_limit:
                continue

            new_pieces = copy.deepcopy(pieces)
            new_pieces.append(new_piece)
            new_matrix = copy.deepcopy(matrix)
            for x, y in new_piece:
                new_matrix[y][x] = piece_id

            face = get_face(new_piece, matrix)

            if len(get_connected_subgraphs(matrix)) > 1:
                continue
            waiting.append([face[0], piece_id + 1, new_pieces, new_matrix, new_pieces2count])

        if random.random() < 0.05:
            random.shuffle(waiting)
        elif random.random() < 0.95:
            waiting = sorted(waiting)            
    return matrix

同じ形のピースが指定個数以下かどうか確認する

同じ形のピースが指定個数以下かどうか確認する関数です。同じ目的のために作ったディクショナリーとして pieces2count を前回使ったので、無駄といえば無駄かもしれません。改良の余地があるところです。

今回この関数を作った理由は、「半分だけ収める計算を行い、その組み合わせの中から条件を満たす組み合わせを発見する方針」に変更したため、半分解いた答えを2つガッチンコした combined_matrix に対して同じ形のピースが指定個数以下かどうか確認したかったからです。

def same_piece_within_limit(matrix, same_piece_limit):
    id2piece = {}
    for y in range(len(matrix)):
        for x in range(len(matrix[y])):
            if matrix[y][x] not in id2piece.keys():
                id2piece[matrix[y][x]] = []
            id2piece[matrix[y][x]].append([x, y])

    key2count = {}
    for id, piece in id2piece.items():
        key = shape_key(piece)
        if key not in key2count.keys():
            key2count[key] = 0
        key2count[key] += 1
        if key2count[key] > same_piece_limit:
            return False

    return True

テトロミノ(レベル1)を解きます

以上の関数を用意したら、テトロミノ(レベル1)が解けるか確認です。

-1 ではなく 0 で初期化するように変更しましたが、それは half_puzzle から出てきた「半分だけ解いたパズル」を組み合わせたとき、ぴったりハマったかどうか判定するときに生きてきます。

実行するたびに違う答えが出てくるはずです。

x_length, y_length, piece_size, same_piece_limit = 8, 5, 4, 2

index = 0
matrix_history = []
keta = int(x_length * y_length / piece_size)
for matrix in half_puzzle(x_length, y_length, piece_size, same_piece_limit):
    for prev_matrix in matrix_history:
        matrix3 = np.flipud(np.fliplr(np.array(matrix)))

        if (prev_matrix + matrix3).min().min() > 0:
            matrix3 += keta
            matrix3 = np.where(matrix3 == keta, 0, matrix3)
            combined_matrix = prev_matrix + matrix3
            if same_piece_within_limit(combined_matrix, same_piece_limit):
                depict(combined_matrix)
                index += 1

    matrix_history.append(matrix)
    if index > 5:
        break

noblock_puzzle4_25_0.png

noblock_puzzle4_25_1.png

noblock_puzzle4_25_2.png

noblock_puzzle4_25_3.png

noblock_puzzle4_25_4.png

noblock_puzzle4_25_5.png

noblock_puzzle4_25_6.png

ペントミノ(レベル2)を解きます。

いよいよ、ペントミノ(レベル2)に挑戦です!

様々な計算効率化を実装したので、今度こそ!

x_length, y_length, piece_size, same_piece_limit = 10, 6, 5, 1

index = 0
matrix_history = []
keta = int(x_length * y_length / piece_size)
for matrix in half_puzzle(x_length, y_length, piece_size, same_piece_limit):
    for prev_matrix in matrix_history:
        matrix3 = np.flipud(np.fliplr(np.array(matrix)))

        if (prev_matrix + matrix3).min().min() > 0:
            matrix3 += keta
            matrix3 = np.where(matrix3 == keta, 0, matrix3)
            combined_matrix = prev_matrix + matrix3
            if same_piece_within_limit(combined_matrix, same_piece_limit):
                depict(combined_matrix)
                index += 1

    matrix_history.append(matrix)
    if index > 5:
        break

noblock_puzzle4_27_0.png

noblock_puzzle4_27_1.png

noblock_puzzle4_27_2.png

noblock_puzzle4_27_3.png

noblock_puzzle4_27_4.png

noblock_puzzle4_27_5.png

解けた〜〜!!!

boxing_puzzle

https://github.com/maskot1977/boxing_puzzle にて、pip install できるコードを公開しています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away