またまた少しだけサイズを大きくした「ペグ・ソリテア」に挑戦しましょう。
上図は「変形三角盤」と呼ばれるペグ・ソリテアです。21 個のマスが少し変わった三角形に並んでいて、そこにペグを配置します。ペグは別のペグをひとつだけ跳び超えることで、任意の方向へ移動することができます。もちろん、着地する地点が空いていなければ、跳び越えることはできません。今回は中央のペグを取り除き、最後にペグが中央にひとつ残る跳び方 (中央補償型の解) の最短手数を求めることにします。
それではプログラムを作りましょう。使用するプログラミング言語は Python3 ですが、盤面が大きくなると Python3 では荷が重いので、今回は PyPy3 (ver 7.3.1) を使うことにします。最初に、ペグの跳び先表を定義します。下図のように座標を定義すると、跳び先表は次のようになります。
リスト : 跳び先表の定義
# 定数
MAX_JUMP = 19
HOLE = 6
SIZE = 21
# 跳び先表
jump_table = [
[(2, 4)], # 0
[(2, 3)], # 1
[(3, 5), (4, 7)], # 2
[(2, 1), (5, 8), (6, 10)], # 3
[(2, 0), (6, 9), (7, 11)], # 4
[(3, 2), (6, 7), (8, 13), (9, 15)], # 5
[(9, 14), (10, 16)], # 6
[(4, 2), (6, 5), (10, 15), (11, 17)], # 7
[(5, 3), (9, 10), (13, 19)], # 8
[(6, 4), (10, 11)], # 9
[(6, 3), (9, 8)], # 10
[(7, 4), (10, 9), (17, 20)], # 11
[(13, 14)], # 12
[(8, 5), (14, 15)], # 13
[(9, 6), (13, 12), (15, 16)], # 14
[(9, 5), (10, 7), (14, 13), (16, 17)],# 15
[(10, 6), (15, 14), (17, 18)], # 16
[(11, 7), (16, 15)], # 17
[(17, 16)], # 18
[(13, 8)], # 19
[(17, 11)], # 20
]
変形三角盤を解く関数 solver() は次のようになります。
リスト : 変形三角盤の解法
def solver():
global cnt
cnt = 0
board = [True] * SIZE
# 初手を 14 -> 6 に限定
board[14] = False
board[9] = False
for i in range(2, MAX_JUMP + 1):
print(f'----- {i} -----')
dfs(board, 1, i, [(14, 6)])
if cnt > 0: break
print(cnt)
今回は初手を 14 -> 6 に限定します。あとのプログラムは Hoppers と同じなので説明は割愛します。詳細はプログラムリストをお読みください。
それでは実行してみましょう。
>>>> s = time.time(); solver(); print(time.time() - s) ----- 2 ----- ----- 3 ----- ----- 4 ----- ----- 5 ----- ----- 6 ----- ----- 7 ----- ----- 8 ----- ----- 9 ----- ----- 10 ----- ----- 11 ----- ----- 12 ----- [14, 6][11, 9][3, 10][1, 3][7, 2][0, 4][12, 14, 6][5, 2, 7, 5, 13][20, 11, 9] [15, 17][19, 8, 10][18, 16, 6] [14, 6][11, 9][3, 10][1, 3][7, 2][0, 4][12, 14, 6][5, 2, 7, 5, 13][20, 11, 9] [19, 8, 10][15, 17][18, 16, 6] [14, 6][11, 9][3, 10][1, 3][7, 2][0, 4][12, 14, 6][5, 7, 2, 5, 13][20, 11, 9] [15, 17][19, 8, 10][18, 16, 6] ・・・省略・・・ [14, 6][11, 9][3, 10][12, 14, 6][20, 11, 9][1, 3][15, 17][7, 2][0, 4][5, 7, 2, 5, 13] [19, 8, 10][18, 16, 6] [14, 6][11, 9][3, 10][12, 14, 6][20, 11, 9][15, 17][1, 3][7, 2][0, 4][5, 2, 7, 5, 13] [19, 8, 10][18, 16, 6] [14, 6][11, 9][3, 10][12, 14, 6][20, 11, 9][15, 17][1, 3][7, 2][0, 4][5, 7, 2, 5, 13] [19, 8, 10][18, 16, 6] 96 579.0008935928345 実行環境 : PyPy3 (ver 7.3.1), Ubunts 20.04 (WSL1), Intel Core i5-6200U 2.30GHz
最短手数は 12 手、解は全部で 96 通りあります。実行時間は約 9 分 40 秒かかりました。解を一つ求めるだけでよければ、もっと速くなります。dfs() の最初の if 文を次のように修正します。
リスト : 解を一つ求める場合
def dfs(board, jc, limit, move):
global cnt
if jc > limit or cnt > 0: return
・・・省略・・・
実行時間は約 5 分 30 秒になりました。それでも、単純な反復深化では時間がかかりますね。そこで、次回は反復深化の常套手段である「下限値枝刈り法」を使ってプログラムの高速化に挑戦しましょう。
#
# peg21.py : 変形三角盤 (ペグ・ソリティア)
#
# Copyright (C) 2022 Makoto Hiroi
#
# 定数
MAX_JUMP = 19
HOLE = 6
SIZE = 21
# 跳び先表
jump_table = [
[(2, 4)], # 0
[(2, 3)], # 1
[(3, 5), (4, 7)], # 2
[(2, 1), (5, 8), (6, 10)], # 3
[(2, 0), (6, 9), (7, 11)], # 4
[(3, 2), (6, 7), (8, 13), (9, 15)], # 5
[(9, 14), (10, 16)], # 6
[(4, 2), (6, 5), (10, 15), (11, 17)], # 7
[(5, 3), (9, 10), (13, 19)], # 8
[(6, 4), (10, 11)], # 9
[(6, 3), (9, 8)], # 10
[(7, 4), (10, 9), (17, 20)], # 11
[(13, 14)], # 12
[(8, 5), (14, 15)], # 13
[(9, 6), (13, 12), (15, 16)], # 14
[(9, 5), (10, 7), (14, 13), (16, 17)],# 15
[(10, 6), (15, 14), (17, 18)], # 16
[(11, 7), (16, 15)], # 17
[(17, 16)], # 18
[(13, 8)], # 19
[(17, 11)], # 20
]
# 手順の表示
def print_move(xs):
from_, to_ = xs[0]
print(f'[{from_}, {to_}', end='')
prev = to_
for from_, to_ in xs[1:]:
if prev == from_:
print(f', {to_}', end='')
else:
print(f'][{from_}, {to_}', end='')
prev = to_
print(']')
# 反復深化
def dfs(board, jc, limit, move):
global cnt
if jc > limit: return
if len(move) == MAX_JUMP:
if board[HOLE]:
print_move(move)
cnt += 1
else:
for from_ in range(SIZE):
if not board[from_]: continue
for del_, to_ in jump_table[from_]:
if not board[del_] or board[to_]: continue
board[from_] = False
board[del_] = False
board[to_] = True
if move[-1][1] == from_:
newjc = jc
else:
newjc = jc + 1
move.append((from_, to_))
dfs(board, newjc, limit, move)
move.pop()
board[from_] = True
board[del_] = True
board[to_] = False
def solver():
global cnt
cnt = 0
board = [True] * SIZE
# 初手を 14 -> 6 に限定
board[14] = False
board[9] = False
for i in range(2, MAX_JUMP + 1):
print(f'----- {i} -----')
dfs(board, 1, i, [(14, 6)])
if cnt > 0: break
print(cnt)