0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ペントミノをExact cover問題として解く(その1)

Last updated at Posted at 2025-05-08

有名なパズルのペントミノ (Wikipedia)を解くコードを紹介します。なるべく分かりやすく、以下のステップでコードを作っていきます。

  1. ペントミノのピースのデータ構造
  2. ボードのデータ構造
  3. まず回転・鏡映はしないで10x6の長方形を作るコードを総当りで作る
  4. Exact cover (Wikipedia)の考え方
  5. Exact cover問題を解くdancing linksのpackage、PyPIのDLXの説明
  6. 回転・鏡映をしたピースのデータを作る
  7. ペントミノをExact Cover問題として考える
  8. DLXを用いて回転・鏡映ありで10x6の長方形を解く

1. ペントミノのピースのデータ構造

image.png

このペントミノの12個のピースを構成する5個の正方形のセルの相対座標を$(x,y)$のリストで定義します。どこを原点にするかで変わってきますがそれは気にしなくても大丈夫です。まずは回転等しなくても答えが出るように上の図形と同じ向きしました。

pentomino = [
    [(2,0),(0,1),(1,1),(2,1),(1,2)], # F/0
    [(0,0),(0,1),(0,2),(0,3),(0,4)], # I/1
    [(0,1),(1,1),(2,1),(3,1),(3,0)], # L/2
    [(1,0),(2,0),(3,0),(0,1),(1,1)], # N/3
    [(0,0),(0,1),(0,2),(1,1),(1,2)], # P/4
    [(0,0),(1,0),(2,0),(1,1),(1,2)], # T/5
    [(0,0),(1,0),(1,1),(0,2),(1,2)], # U/6
    [(2,0),(2,1),(0,2),(1,2),(2,2)], # V/7
    [(1,0),(2,0),(0,1),(1,1),(0,2)], # W/8
    [(1,0),(0,1),(1,1),(2,1),(1,2)], # X/9
    [(1,0),(0,1),(1,1),(2,1),(3,1)], # Y10
    [(1,0),(2,0),(1,1),(0,2),(1,2)]] # Z/11

2. ボードのデータ構造

10x6の長方形を(x,y)の1次元リストで表します。

X, Y = 10, 6
board = [(x,y) for y in range(Y) for x in range(X)]

image.png

3. 回転・鏡映はしないで10x6の長方形を作るコードを総当りで作る

この準備として以下の2点を行います。

ピース上の最初のセルを原点に変換する

これによってピースの原点をボードの空きセルに合わせれば移動したピースの位置の候補が列挙できるようになります。

def addxy(xy1, xy2, s=1):
  (x1, y1), (x2, y2)  = xy1, xy2
  return (x1+s*x2, y1+s*y2)
# Normalize (no rotate/reflect)
pieces = []
for pn, pc in enumerate(pentomino):
  pc1 = [(pn,0), [addxy(c,pc[0],-1) for c in pc]]
  print(pc1)
  pieces += [pc1]
#  [(0, 0), [(0, 0), (-2, 1), (-1, 1), (0, 1), (-1, 2)]]
#  [(1, 0), [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)]]
#  [(2, 0), [(0, 0), (1, 0), (2, 0), (3, 0), (3, -1)]]
#  [(3, 0), [(0, 0), (1, 0), (2, 0), (-1, 1), (0, 1)]]
#  [(4, 0), [(0, 0), (0, 1), (0, 2), (1, 1), (1, 2)]]
#  [(5, 0), [(0, 0), (1, 0), (2, 0), (1, 1), (1, 2)]]
#  [(6, 0), [(0, 0), (1, 0), (1, 1), (0, 2), (1, 2)]]
#  [(7, 0), [(0, 0), (0, 1), (-2, 2), (-1, 2), (0, 2)]]
#  [(8, 0), [(0, 0), (1, 0), (-1, 1), (0, 1), (-1, 2)]]
#  [(9, 0), [(0, 0), (-1, 1), (0, 1), (1, 1), (0, 2)]]
#  [(10, 0), [(0, 0), (-1, 1), (0, 1), (1, 1), (2, 1)]]
#  [(11, 0), [(0, 0), (1, 0), (0, 1), (-1, 2), (0, 2)]] 

ボードの空きエリアをsetで表す

こうすることによってピースが空きエリアに収まるかどうかを簡単に表現できます。

freearea = set(board)

総当たりのコード

stackには(ピース番号、置いた座標のリスト、空きエリア)が入ります。次のピース入るかをチェックして入れば、その分空きエリアを狭めて行き、空きエリアが0になればゴールです。

stack = [(0, [], freearea)] # piece number, position list, free area
while stack:
  pn, st, freearea = stack.pop()
  if len(freearea) == 0: break  # Answer found
  for fcell in freearea:
    cells = {addxy(fcell,p) for p in pieces[pn][1]} # move pieces all over the free area
    if cells <= freearea:                           # if cells fits in free area
      stack += [(pn+1, st+[fcell], freearea - cells)] # decrease free area

print("***** Goal : ", st, freearea)
# ***** Goal :  [(6, 1), (0, 1), (5, 4), (5, 0), (1, 3), (0, 0), (8, 0), (9, 3), (3, 0), (3, 2), (4, 4), (7, 1)] set()

このコードでピースが入るかどうかの判定はこの1行で書けることに注目して下さい。

if cells <= free: # if cells fits in free area

出た答えはピース0の原点が$(6,1)$, ピース1の原点が$(0,1)$, ...となることを示しています。この回答を図示して最初の図形と一致しているのを確認しました。

image.png

ただこの全探索だと回転・鏡映なしでも約4分かかるので、画期的な高速化が必要をなります。そのためにこの問題をExact cover問題に置き換えて考える方法を次回のその2で説明したいと思います。

(開発環境:Google Colab)

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?