ナンプレ大好き人間が、Python初心者だけど、ChatGPT 4oの力を借りて、「ナンプレ問題作成プログラム」を作ってみた・・・その3。
ChatGPTに、改善案を提案して、出力されたプログラムを実行して、おかしな部分をフィードバックして、また出力されたプログラムを実行して、さらなるフィードバック…。
こんなことを午前中から、ついさっきまで何回も繰り返した。
どうにか、以前の問題点を改善できたので、「その3」を書きました。
ちなみに、前回での課題はこれです。
- 問題の難しさを左右するのは、空白個数だけではないみたい
- それに変わるものは何?
改善の方向性は…
前掲の課題を改善する方向性を考え、ChatGPT 4oとのやりとりを重ねる。
- 初級の難度は、いまのままで良いと判断
- 中級以上には、これまで以上の難易度を付加する
- 難問については、これまでの空白55個から、空白60個で実行
- 実行結果、時間が掛かりすぎて、問題出力できない
- アルゴリズムの改善を図るも上手くいかない
- 結果、難問については、空白57に変更することに
- 問題作成&解法については、次の3項目のルーチンをフラグで取り込めるような仕様にした
- X-Wing、WY-Wing、Swordfish
- これで出力した、初級・中級・上級・難問を解く
- これまで以上に、難易度がアップした
細かい出力などについても、修正を重ねた。
修正後のプログラム(Ver 0.69)
相変わらず、コピーはできませんが…。
import random
import time # 処理時間を計測するためのライブラリ
# 特定のセルに数字を配置できるかを確認する関数
def is_valid(board, row, col, num):
for i in range(9):
if board[row][i] == num: # 同じ行に同じ数字がある場合
return False
if board[i][col] == num: # 同じ列に同じ数字がある場合
return False
start_row, start_col = 3 * (row // 3), 3 * (col // 3)
for i in range(start_row, start_row + 3):
for j in range(start_col, start_col + 3):
if board[i][j] == num: # ブロック内に同じ数字がある場合
return False
return True
# 初期候補リストの生成関数
def generate_candidates(board):
candidates = [[set(range(1, 10)) for _ in range(9)] for _ in range(9)]
for i in range(9):
for j in range(9):
if board[i][j] != 0:
candidates[i][j] = set()
else:
for num in range(1, 10):
if not is_valid(board, i, j, num):
candidates[i][j].discard(num)
return candidates
# X-Wing テクニックを用いて候補を削除する関数
def apply_x_wing(board, candidates):
x_wing_found = False
# X-Wing のロジックを実装
return x_wing_found
# XY-Wing テクニックを用いて候補を削除する関数
def apply_xy_wing(board, candidates):
xy_wing_found = False
# XY-Wing のロジックを実装
return xy_wing_found
# Swordfish テクニックを用いて候補を削除する関数
def apply_swordfish_routine(board, candidates):
swordfish_found = False
# Swordfish のロジックを実装
return swordfish_found
# ナンプレ盤面を解くための再帰的なバックトラッキングアルゴリズムを実装した関数
def solve_sudoku(board, candidates, apply_xwing=False, apply_xywing=False, apply_swordfish=False):
if apply_xwing: # X-Wing を適用するかどうか
x_wing_applied_count = 0 # X-Wing の適用回数を制限するためのカウンター
while apply_x_wing(board, candidates):
x_wing_applied_count += 1
if x_wing_applied_count > 2: # X-Wing の適用回数を2回に制限
break
if apply_xywing: # XY-Wing を適用するかどうか
xy_wing_applied_count = 0 # XY-Wing の適用回数を制限するためのカウンター
while apply_xy_wing(board, candidates):
xy_wing_applied_count += 1
if xy_wing_applied_count > 2: # XY-Wing の適用回数を2回に制限
break
if apply_swordfish: # Swordfish を適用するかどうか
swordfish_applied_count = 0 # Swordfish の適用回数を制限するためのカウンター
while apply_swordfish_routine(board, candidates): # 関数呼び出しを修正
swordfish_applied_count += 1
if swordfish_applied_count > 2: # Swordfish の適用回数を2回に制限
break
for i in range(9):
for j in range(9):
if board[i][j] == 0:
for num in candidates[i][j]:
if is_valid(board, i, j, num):
board[i][j] = num
new_candidates = generate_candidates(board)
if solve_sudoku(board, new_candidates, apply_xwing, apply_xywing, apply_swordfish): # 再帰的に次の空白セルを解こうとする
return True
board[i][j] = 0 # バックトラック
return False # 全ての数字が試されて無効だった場合、Falseを返す
return True # すべてのセルが埋められた場合
# ナンプレの完全な9x9盤面を生成するための関数
def generate_complete_board_backtracking():
def fill_board(board):
for i in range(9):
for j in range(9):
if board[i][j] == 0:
random_nums = list(range(1, 10))
random.shuffle(random_nums)
for num in random_nums:
if is_valid(board, i, j, num):
board[i][j] = num
if fill_board(board):
return True
board[i][j] = 0
return False
return True
board = [[0] * 9 for _ in range(9)]
fill_board(board)
return board
# 完成したナンプレ盤面から数字を削除し、指定された数の空白セルを作成するための関数
def remove_numbers_strict(board, holes):
attempts = holes
while attempts > 0:
row, col = random.randint(0, 8), random.randint(0, 8)
while board[row][col] == 0:
row, col = random.randint(0, 8), random.randint(0, 8)
backup = board[row][col]
board[row][col] = 0
board_copy = [row[:] for row in board]
if count_solutions(board_copy) != 1:
board[row][col] = backup
else:
if count_solutions(board) == 1:
attempts -= 1
else:
board[row][col] = backup
return board
# ナンプレの盤面がいくつかの解を持っているかをカウントするための関数
def count_solutions(board):
def solve(board):
nonlocal count
for i in range(9):
for j in range(9):
if board[i][j] == 0:
for num in range(1, 10):
if is_valid(board, i, j, num):
board[i][j] = num
solve(board)
board[i][j] = 0
return
count += 1
count = 0
solve(board)
return count
# ナンプレの盤面を視覚的に表示するための関数
def print_sudoku(board):
print("+-------+-------+-------+")
for i in range(9):
row = "| "
for j in range(9):
if board[i][j] == 0:
row += " "
else:
row += str(board[i][j]) + " "
if (j + 1) % 3 == 0:
row += "| "
print(row)
if (i + 1) % 3 == 0:
print("+-------+-------+-------+")
# ナンプレの盤面を解いて、その結果を表示するための関数
def solve_sudoku_and_print(board, level, apply_xwing=False, apply_xywing=False, apply_swordfish=False):
start_time = time.time() # 処理開始時間を記録
print(f"\n【{level}】\n★ただいま解答表示準備中...")
candidates = generate_candidates(board)
if solve_sudoku(board, candidates, apply_xwing, apply_xywing, apply_swordfish):
print_sudoku(board)
else:
print("解けない")
end_time = time.time() # 処理終了時間を記録
elapsed_time = end_time - start_time
print(f"処理時間: {elapsed_time:.2f} 秒") # 処理時間を表示
# ナンプレの問題を指定された難易度に基づいて生成し、その問題を解いて表示するための関数
def sudoku_solver_process(level, apply_xwing=False, apply_xywing=False, apply_swordfish=False):
# 使用するテクニックに応じたメッセージを生成
techniques = []
if apply_xwing:
techniques.append("X-Wing適用")
if apply_xywing:
techniques.append("XY-Wing適用")
if apply_swordfish:
techniques.append("Swordfish適用")
technique_message = " ".join(techniques)
print(f"\n【{level}】 {technique_message}\n★ただいま問題作成中...")
start_time = time.time() # 処理開始時間を記録
if level == "初級":
holes = 30
elif level == "中級":
holes = 40
elif level == "上級":
holes = 50
elif level == "難問":
holes = 57
else:
print("無効なレベルです。レベルは「初級」「中級」「上級」「難問」から選んでください。")
return
complete_board = generate_complete_board_backtracking()
puzzle = remove_numbers_strict(complete_board, holes)
print_sudoku(puzzle)
solve_sudoku_and_print(puzzle, level, apply_xwing, apply_xywing, apply_swordfish)
end_time = time.time() # 処理終了時間を記録
elapsed_time = end_time - start_time
print(f"総処理時間: {elapsed_time:.2f} 秒") # 処理時間を表示
# 例: 難問レベルのナンプレ問題を生成して解く
# X-Wing、XY-Wing、Swordfish適用を選択
sudoku_solver_process("初級", apply_xwing=False, apply_xywing=False, apply_swordfish=False)
sudoku_solver_process("中級", apply_xwing=False, apply_xywing=False, apply_swordfish=False)
sudoku_solver_process("上級", apply_xwing=True, apply_xywing=False, apply_swordfish=False)
sudoku_solver_process("上級", apply_xwing=False, apply_xywing=True, apply_swordfish=False)
sudoku_solver_process("上級", apply_xwing=False, apply_xywing=False, apply_swordfish=True)
sudoku_solver_process("難問", apply_xwing=True, apply_xywing=False, apply_swordfish=False)
sudoku_solver_process("難問", apply_xwing=False, apply_xywing=True, apply_swordfish=False)
sudoku_solver_process("難問", apply_xwing=False, apply_xywing=False, apply_swordfish=True)
出力した問題は…
以下、Ver 0.69で生成した問題です。
なお、解答は割愛します。
解答の一意性はチェック済です。
初級 005
初級問題のアルゴリズムは、原則、これまでと同じ。
つまり、空白個数は30個、X-Wing等は取り込まず。
なお、処理時間を出力するようにしました。
ちなみに、初級でも、フラグ設定で、X-Wing、XY-Wing、Swordfishは使えます。
生成処理(問題出力&解答出力)に要した時間は、0.02秒。
問題としても、簡単で、初級としては十分OKです。
中級 005
中級問題も、初級と同じ。
つまり、フラグ設定で、X-Wing等使えますが、今回、中級は、空白40個だけで難易度を設定することにしました。
今まで通りということです。
生成処理(問題出力&解答出力)は、0.11秒。
問題としては、中級としては、少し簡単すぎます。
フラグ設定で、いろいろやってみます。
上級 005〜007
上級問題については、空白設定50個で、X-Wing、XY-Wing、Swordfishの3パターンで作ってみた。
上級 005 X-Wing
生成処理(問題出力&解答出力)は、4.32秒。
上級 XY-Wing
生成処理(問題出力&解答出力)は、0.16秒。
上級 Swordfish
生成処理(問題出力&解答出力)は、0.17秒。
難問 005〜007
難問は、空白個数 57、X-Wing・XY-Wing・Swordfishの3パターンを作成。
難問 005 X-Wing
生成処理(問題出力&解答出力)は、8.00秒。
難問 006 XY-Wing
生成処理(問題出力&解答出力)は、2.29秒。
難問 007 Swordfish
生成処理(問題出力&解答出力)は、6.13秒。
まとめ 今後の課題は…
ChatCPT 4oのプログラムを100%理解しているワケではないことが、1つの問題。
つまり、X-Wingの関数、XY-Wingの関数、Swordfishの関数が、よく理解できない。
理解できるまで、繰り返しChatGPTして、教えてもらいますが(笑)。
ところで、実際に問題を解いた実感としては、
XY-WingルーチンとSwordfishルーチンを使ったものは、「解くのが簡単」という感じが否めない。
以前のプレーンなものよりは、格段に難しいが、難しさを解いたという満足感に欠ける。
一方、X-Wingルーチンを使ったものは、それなりに難易度が高い。
今後はこのあたりの改善をしたい。
それから、難問の空白設定を60にしても、処理速度が掛からない、アルゴリズムを考えたい。
で、今後の課題のまとめ。
- 難問の空白設定は60で作ってみたい。
- その場合、処理速度が掛かりすぎるので、それを回避するアルゴリズムを考えたい
- XY-Wing関数、Swordfish関数を使った場合、難易度的には満足感が少ない
- もっと、難易度が高いものを作れるようにしたい