LoginSignup
5
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-25

ジグソーパズルではなくて、何も描いていないピースをケースから取り出して、それをきれいにケースに戻す「箱詰めパズル」、ありますよね。たとえば、こういうやつです。

うちの子らはこのブロックを夢中になって解いているのですが、私はこれを解くプログラムをPythonで書いてみました。

まずはケースを用意します

横の長さ x 縦の長さ y のケースを用意します。まずは、10未満のランダムな整数がケースに入るようにします。

import random
def generate(x, y):
    return [[random.randrange(10) for _ in range(x)] for _ in range(y)]

これを図示できるようにしましょう。

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

図示します。

matrix = generate(8, 5)
depict(matrix)
matrix

noblock_puzzle_5_0.png

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

これでケースはできました。次からは、この「色」(整数)が「ピース」を表すことにします。

ピースを置く準備をします

ケースの中で、何もピースを置いていないマスをに整数 -1 を置きましょう。

def generate(x, y):
    return [[-1 for _ in range(x)] for _ in range(y)]

matrix = generate(8, 5)
depict(matrix)
matrix

noblock_puzzle_7_0.png

[[-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1]]

隅に 0 の正方形を置きます。これだけではパズルのピースにはなりませんね。

x_length = 8
y_length = 5
matrix = generate(x_length, y_length)

piece_id = 0
x = 0
y = 0
matrix[y][x] = piece_id

depict(matrix)
matrix

noblock_puzzle_9_0.png

[[0, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1]]

ピースを置きます

実際のパズルでは、たとえば正方形が4つ連なったような形をしています。これを作ってみましょう。

まず、最初に置いた正方形(またはその集合)の隣の正方形を得るための関数を作ります。

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] == -1:
                neighbors.append([x - dx, y - dy])

    return neighbors

上のような動作を、指定された回数だけ繰り返して、望みの大きさの「ピース」を作りましょう。

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

    for x in range(x_length):
        for y in range(y_length):
            if matrix[y][x] == -1:
                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つ置いてみます。

x_length = 8
y_length = 5
piece_size = 4
matrix = generate(x_length, y_length)

piece_id = 0
piece = get_new_piece(matrix, piece_size)
for x, y in piece:
    matrix[y][x] = piece_id

depict(matrix)
matrix

noblock_puzzle_15_0.png

[[0, -1, -1, -1, -1, -1, -1, -1],
 [0, 0, 0, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1],
 [-1, -1, -1, -1, -1, -1, -1, -1]]

それっぽくなりましたね。ピースが占有するマスは 0 以上の整数が与えられ、ピースに占有されていないマスは -1 のままになっています。

置けるところまで置いてみます

次は、-1 のままになっているマスに、次々にピースを置いていきましょう。置けるところまで置いてみます。

x_length = 8
y_length = 5
piece_size = 4
matrix = generate(x_length, y_length)

piece_id = 0
piece = get_new_piece(matrix, piece_size)

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

    piece = get_new_piece(matrix, piece_size)
    piece_id += 1

matrix

noblock_puzzle_17_0.png

noblock_puzzle_17_1.png

noblock_puzzle_17_2.png

noblock_puzzle_17_3.png

noblock_puzzle_17_4.png

noblock_puzzle_17_5.png

noblock_puzzle_17_6.png

noblock_puzzle_17_7.png

[[0, 0, 3, 3, 3, -1, 6, -1],
 [0, 0, 4, 3, 6, 6, 6, -1],
 [1, 1, 4, 4, 4, -1, -1, -1],
 [1, 1, 5, 5, 5, 5, 7, -1],
 [2, 2, 2, 2, 7, 7, 7, -1]]

ピースの形の選び方はランダムに決まるため、運が良ければ最後までたどり着いて完成しますが、普通は、途中で行き詰まって完成せずに終了します。

同じ形のピースの数には制限があります

さて、実際のパズルでは、同じ形のピースをいくつも選べたりはしません。同じ形のピースの数には制限があります。そこで、ピースの形を判定して、同じ形のピースの個数を数えながら、制限を超えない範囲でピースを選ぶようにしましょう。

def shape_key(piece):
    distances = []
    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)
    return "".join(str(sorted(distances)))

上記のコードで、同じ形のピースは同じ key が割り当てられるようにしました。こうすることで、問題としてはもっと難しくなってしまいました。

レベル1「テトロミノ」に挑戦!

完成するまでひたすら試行錯誤を繰り返すコードは次のようになります。

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 = 0
    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 x == -1 else 0 for x in mat]) for mat in matrix])

    if best_score > score:
        best_score = score
        best_matrix = matrix

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

depict(best_matrix)
best_matrix
1133 th trial

noblock_puzzle_23_1.png

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

最初に説明した脳ブロックのうち、レベル1とされる「テトロミノ」は、これで解けるようになりました。ランダムにピースを選んでいくので、実行するたびに違う解が得られます。

レベル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 = 0
    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 x == -1 else 0 for x in mat]) for mat in matrix])

    if best_score > score:
        best_score = score
        best_matrix = matrix

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

depict(best_matrix)
best_matrix

noblock_puzzle_25_0.png

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

...と思ったら、なかなか解けませんね。私も何度も計算し直しましたが、レベル2のペントミノは組み合わせの数が多すぎて、このコードでは正解にたどり着けないようです。レベル2で、これか。おそるべし。

boxing_puzzle

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

レベル2

続編記事として 何百~何千通りも答えのある箱詰めパズルを解く(レベル2:ペントミノ) を書きましたので、合わせてお読みください。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2