ペグ・ソリテアは、盤上に配置されたペグ (駒) を最後にはひとつ残るように取り除いていく古典的なパズルです。ペグは次のルールに従って移動して取り除くことができます。
今回の問題は盤面を「六芒星」の形にしたペグ・ソリテアです。
○ / \ ●───●───●───● \ / \ / \ / ●───●───● / \ / \ / \ ●───●───●───● \ / ● 問題 : ペグ・ソリテア・スター
上図では黒丸でペグを表し、白丸で空き場所を表しています。ペグは線に沿って移動することができます。最初に取り除いたペグ(白丸)と最後に残ったペグの位置が同じになる「補償型の解」の最短手順を求めてください。
ところで、この六芒星盤のペグ・ソリテアですが、柏木 或 さんのホームページ「裂け目の向こう」の「パズルトピア攻略 - [ダビデの星]」によると、『ちなみにこの六芒星状の配置は芦ヶ原氏によるオリジナルです。かつては東洋ガラスから同名の製品が市販されていました。』 とのことです。
今回は Python3 (ver 3.8.10) でプログラムを作ります。このパズルはペグの個数が少ないので、単純な「反復深化」で十分です。反復深化のプログラムは今までに何度も作っているので、説明は省略いたします。反復深化の説明は拙作のページ「ペグ・ソリテア」をお読みくださいませ。
○ 0 / \ / \ ●───●───●───● 1───2───3───4 \ / \ / \ / \ / \ / \ / ●───●───● 5───6───7 / \ / \ / \ / \ / \ / \ ●───●───●───● 8───9───10───11 \ / \ / ● 12 図 : ペグ・ソリテア・スター
それでは、パズル「ペグ・ソリテア・スター」の解答を示します。上図のように穴に番号をつけて、ペグの移動は番号で表すことにします。移動手順は 1 手を [ 移動元 (from), 移動先 (to) ] で表し、連続跳び越しの場合は [from, to1, to2, ..., to3] とします。
実行結果は次のようになりました。
>>> s = time.time(); solver(7, 3, 0); print(time.time() - s) ----- 2 ----- ----- 3 ----- ----- 4 ----- ----- 5 ----- ----- 6 ----- ----- 7 ----- [7, 0][1, 3][8, 2][12, 5, 7][11, 9][4, 10][0, 5, 12, 7, 0] [7, 0][1, 3][8, 2][12, 5, 7][11, 9][4, 10][0, 7, 12, 5, 0] [7, 0][1, 3][12, 7][4, 2, 10][8, 2][11, 3][0, 5, 12, 7, 0] [7, 0][1, 3][12, 7][4, 2, 10][8, 2][11, 3][0, 7, 12, 5, 0] [7, 0][1, 3][12, 7][4, 2, 10][11, 3][8, 2][0, 5, 12, 7, 0] [7, 0][1, 3][12, 7][4, 2, 10][11, 3][8, 2][0, 7, 12, 5, 0] [7, 0][1, 3][12, 7][8, 2, 10][4, 2][11, 3][0, 5, 12, 7, 0] [7, 0][1, 3][12, 7][8, 2, 10][4, 2][11, 3][0, 7, 12, 5, 0] [7, 0][1, 3][12, 7][8, 10][11, 9][4, 10, 8, 2][0, 5, 7, 0] [7, 0][1, 3][12, 7][8, 10][11, 9][4, 10, 8, 2][0, 7, 5, 0] [7, 0][12, 7][1, 3][4, 2, 10][8, 2][11, 3][0, 5, 12, 7, 0] [7, 0][12, 7][1, 3][4, 2, 10][8, 2][11, 3][0, 7, 12, 5, 0] [7, 0][12, 7][1, 3][4, 2, 10][11, 3][8, 2][0, 5, 12, 7, 0] [7, 0][12, 7][1, 3][4, 2, 10][11, 3][8, 2][0, 7, 12, 5, 0] [7, 0][12, 7][1, 3][8, 2, 10][4, 2][11, 3][0, 5, 12, 7, 0] [7, 0][12, 7][1, 3][8, 2, 10][4, 2][11, 3][0, 7, 12, 5, 0] [7, 0][12, 7][1, 3][8, 10][11, 9][4, 10, 8, 2][0, 5, 7, 0] [7, 0][12, 7][1, 3][8, 10][11, 9][4, 10, 8, 2][0, 7, 5, 0] [7, 0][12, 7][8, 10][1, 3][11, 9][4, 10, 8, 2][0, 5, 7, 0] [7, 0][12, 7][8, 10][1, 3][11, 9][4, 10, 8, 2][0, 7, 5, 0] [7, 0][12, 7][8, 10][1, 9, 3][11, 9][4, 10][0, 5, 12, 7, 0] [7, 0][12, 7][8, 10][1, 9, 3][11, 9][4, 10][0, 7, 12, 5, 0] [7, 0][12, 7][8, 10][11, 9][1, 3][4, 10, 8, 2][0, 5, 7, 0] [7, 0][12, 7][8, 10][11, 9][1, 3][4, 10, 8, 2][0, 7, 5, 0] [7, 0][12, 7][8, 10][11, 9, 3][1, 9][4, 10][0, 5, 12, 7, 0] [7, 0][12, 7][8, 10][11, 9, 3][1, 9][4, 10][0, 7, 12, 5, 0] [7, 0][12, 7][8, 10][11, 9, 3][4, 10][1, 9][0, 5, 12, 7, 0] [7, 0][12, 7][8, 10][11, 9, 3][4, 10][1, 9][0, 7, 12, 5, 0] 28 0.10868620872497559 実行環境 : Python3 (ver 3.8.10), Ubunts 20.04 (WSL1), Intel Core i5-6200U 2.30GHz
7 手で解くことができました。最短手順の一例を示します。
1: [7, 0] 2: [12, 7] 3: [8, 10] 4: [11, 9, 3] 5: [4, 10] 6: [1, 9] 7: [0, 7, 12, 5, 0]
「補償型の解」という条件がない場合でも最短手数は 7 手になります。また、最初に取り除くペグの位置が 2 の場合も最短手数は 7 手になります。
● / \ ●───●───●───● \ / \ / ● ● / \ / \ ●───●───●───● \ / ● 図 : ペグ・ソリテア・スター(その2)
これではもの足りないという方のために、もうひとつ問題を出しましょう。上図のように中央のペグを取り除いた盤を考えてみます。最初にどれかひとつペグを取り除いて、最後にペグがひとつ残る最短手順を求めてください。ただし、最初に取り除くペグの位置によっては解けない場合があります。ご注意くださいませ。
拙作のページ「偶奇性のお話」で説明しましたが、ペグ・ソリテアには「偶奇性」があります。ペグ・ソリテア・スターの場合、下図のように 3 つのグループに分けると、グループに属するペグ数と全体のペグ数の偶奇性を使って、解の有無をチェックすることができます。
● 0 / \ / \ Group 0 : 7 ●───●───●───● 0───1───2───0 \ / \ / \ / \ / \ / \ / Group 1 : 3 ●───●───● 2───0───1 / \ / \ / \ / \ / \ / \ Group 2 : 3 ●───●───●───● 0───1───2───0 \ / \ / Total : 13 ● 0 図 : ペグ・ソリテア・スターの偶奇性
最初にペグをどれかひとつ取り除いた状態で、3 つのグループのなかの 1 グループだけが全体のペグ数の偶奇性と一致していないと、そのペグ・ソリテアには解がありません。上図のペグ・ソリテア・スターの場合、最初にペグをひとつ取り除くと、そのグループのペグ数は偶数になりますが、ほかのグループは奇数のままですね。そして、全体のペグ数は偶数になるので偶奇性の条件を満たします。ただし、この関係を満たしているからといって、必ず解けるとはかぎらないことに注意してください。
それでは、ペグ・ソリテア・スター(その2)の場合はどうなるでしょうか。
● 0 / \ / \ Group 0 : 6 ●───●───●───● 0───1───2───0 \ / \ / \ / \ / Group 1 : 3 ● ● 2 1 / \ / \ / \ / \ Group 2 : 3 ●───●───●───● 0───1───2───0 \ / \ / Total : 12 ● 0 図 : ペグ・ソリテア・スター(その2)の偶奇性
最初に Group 0 のペグをひとつ取り除くと、3 つのグループすべて奇数になってしまいますね。この場合、解くことはできません。では、Group 1 のペグをひとつ取り除いてみましょう。すると、全体のペグ数は奇数になり、Group 0, 1 は偶数で Group 2 は奇数なので偶奇性の条件を満たします。同様に、Group 2 のペグをひとつ取り除いた場合も条件を満たします。
最初に Group 0 のペグを取り除くと解はありませんが、Group 1 か 2 のペグを取り除く場合は解があるかもしれません。実際に解いてみると、最短手順は次のようになりました。
0 2 のペグを取り除いた場合 / \ 1 : [7, 2] 1───2───3───4 2 : [11, 5] \ / \ / 3 : [10, 8] 5 6 4 : [4, 9, 7] / \ / \ 5 : [0, 6] 7───8───9───10 6 : [1, 3] \ / 7 : [7, 2, 4, 9] 11 図 : ペグ・ソリテア・スター(その2)の解答
この場合、「補償型の解」は存在しません。これも偶奇性からわかります。最後にひとつ残るペグは、全体のペグ数の偶奇性と一致しているグループになります。つまり、最初に取り除いたペグのグループが全体のペグ数の偶奇性と一致していることが、「補償型の解」が存在する条件になるのです。
上図の場合、取り除いたペグのグループと全体のペグ数の偶奇性は一致しません。したがって、最初に Group 1 のペグを取り除くと最後に残るペグは Group 2 になり、逆に Group 2 のペグを取り除くと最後に残るペグは Group 1 になるのです。
# # pegstar.py : ペグ・ソリティア・スター # # Copyright (C) 2022 Makoto Hiroi # # 定数 MAX_JUMP = 11 SIZE = 13 # 跳び先表 jump_table = [ [(2, 5), (3, 7)], # 0 [(2, 3), (5, 9)], # 1 [(3, 4), (5, 8), (6, 10)], # 2 [(2, 1), (6, 9), (7, 11)], # 3 [(3, 2), (7, 10)], # 4 [(2, 0), (6, 7), (9, 12)], # 5 [], # 6 [(3, 0), (6, 5), (10, 12)], # 7 [(5, 2), (9, 10)], # 8 [(5, 1), (6, 3), (10, 11)], # 9 [(6, 2), (7, 4), (9, 8)], # 10 [(7, 3), (10, 9)], # 11 [(9, 5), (10, 7)], # 12 ] # 手順の表示 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(from_, del_, to_): global cnt, HOLE cnt = 0 board = [True] * SIZE # 初手を from_ -> to_ に限定 board[from_] = False board[del_] = False HOLE = to_ for i in range(2, MAX_JUMP + 1): print(f'----- {i} -----') dfs(board, 1, i, [(from_, to_)]) if cnt > 0: break print(cnt)
# # pegstar2.py : ペグ・ソリティア・スター2 # # Copyright (C) 2022 Makoto Hiroi # # 定数 MAX_JUMP = 10 SIZE = 12 # 跳び先表 jump_table = [ [(2, 5), (3, 6)], # 0 [(2, 3), (5, 8)], # 1 [(3, 4), (5, 7)], # 2 [(2, 1), (6, 10)], # 3 [(3, 2), (6, 9)], # 4 [(2, 0), (8, 11)], # 5 [(3, 0), (9, 11)], # 6 [(5, 2), (8, 9)], # 7 [(5, 1), (9, 10)], # 8 [(6, 4), (8, 7)], # 9 [(6, 3), (9, 8)], # 10 [(8, 5), (9, 6)], # 11 ] # 手順の表示 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: 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(from_, del_, to_): global cnt cnt = 0 board = [True] * SIZE # 初手を from_ -> to_ に限定 board[from_] = False board[del_] = False for i in range(2, MAX_JUMP + 1): print(f'----- {i} -----') dfs(board, 1, i, [(from_, to_)]) if cnt > 0: break print(cnt)