Python初心者がナンプレ問題作成プログラムを作ってみた-002(プログラムコード有り)

  • URLをコピーしました!
 *本記事を含め、当サイトでは広告を掲載しています。

前回投稿(プログラム Ver 0.1)で、レベル「難問」の場合、解答が複数あるケースが出現。

これを回避すべく若干の手直しをしました。

合わせて、あとで自分で検証しやすくするために、ソースコードにコメントをたっぷり入れました(笑)。

スポンサーリンク

目次

若干の手直し(Ver 0.51)

偉そーに言っていますが、ChatGPT 4o君に依存しています(爆)。

それでは…

相変わらずコピー防止プラグインのため、コピーは出来ないのですが、以下、ソースコードを載せます。

Ver 0.51です。

手直しポイントは2点。

  • 「難問」作成で、解答が2つ発生したので、その改良
  • 自分自身のために、ソースにコメントを大量に入れた(笑)
import random  # 乱数を生成するためのライブラリをインポート

# 特定のセルに数字を配置できるかを確認する関数
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
    
    # 3x3のブロックの開始位置を計算
    start_row, start_col = 3 * (row // 3), 3 * (col // 3)
    
    # 3x3のブロック内をチェックして、同じ数字がないか確認
    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 solve_sudoku(board):
    # 盤面の全ての行をループ
    for row in range(9):
        # 各行の全ての列をループ
        for col in range(9):
            # 空白セル(0が格納されているセル)を見つけた場合
            if board[row][col] == 0:
                # 1から9までの数字を順に試す
                for num in range(1, 10):
                    # 数字がそのセルに配置できるかをチェック
                    if is_valid(board, row, col, num):
                        # 配置可能な場合、そのセルに数字を配置
                        board[row][col] = num
                        
                        # 再帰的に次の空白セルを解こうとする
                        if solve_sudoku(board):
                            return True
                        
                        # 再帰呼び出しが失敗した場合、配置を元に戻す(バックトラック)
                        board[row][col] = 0
                
                # 1から9のいずれの数字も有効でない場合、Falseを返してバックトラックを促す
                return False
    
    # 全てのセルが埋められた場合(空白セルがない場合)、Trueを返す
    return True


# ナンプレの盤面がいくつかの解を持っているかをカウントするための関数
def count_solutions(board):
    # 内部関数 solve を定義して、数独の解をカウントする
    def solve(board):
        nonlocal count  # 外側の count 変数を参照するために nonlocal 宣言
        # 盤面の全ての行をループ
        for row in range(9):
            # 各行の全ての列をループ
            for col in range(9):
                # 空白セル(0が格納されているセル)を見つけた場合
                if board[row][col] == 0:
                    # 1から9までの数字を順に試す
                    for num in range(1, 10):
                        # 数字がそのセルに配置できるかをチェック
                        if is_valid(board, row, col, num):
                            # 配置可能な場合、そのセルに数字を配置
                            board[row][col] = num
                            
                            # 再帰的に次の空白セルを解こうとする
                            solve(board)
                            
                            # 再帰呼び出しが完了したら、配置を元に戻す(バックトラック)
                            board[row][col] = 0
                    # すべての数字が試されたら、リターンしてバックトラック
                    return
        
        # すべてのセルが埋められた場合(空白セルがない場合)、解のカウントを増やす
        count += 1
    
    count = 0  # 解の数をカウントする変数
    solve(board)  # 内部関数を呼び出して解を探索
    return count  # 解の数を返す


# 完成したナンプレ盤面から数字を削除し、指定された数の空白セル(ホール)を作成するための関数
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


# ナンプレの完全な9x9盤面を生成するための関数
def generate_complete_board_backtracking():
    # 内部関数 fill_board を定義して、盤面を再帰的に埋める
    def fill_board(board):
        # 9x9の盤面全体をチェックする
        for i in range(9):
            for j in range(9):
                # 空白セル(値が0のセル)を見つけた場合
                if board[i][j] == 0:
                    # 1から9までの数字をランダムな順序で試す
                    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
                    
                    # 1から9のいずれの数字も配置できない場合、Falseを返してバックトラックを促す
                    return False
        
        # すべてのセルが適切に埋められた場合、Trueを返す
        return True
    
    # 9x9の空の数独盤面を初期化
    board = [[0] * 9 for _ in range(9)]
    
    # 盤面を埋める
    fill_board(board)
    
    # 完成した盤面を返す
    return board


# ナンプレの問題を指定された難易度に基づいて生成するための関数
def generate_sudoku_strict(difficulty):
    # 完全に埋められた数独盤面を生成
    complete_board = generate_complete_board_backtracking()
    
    # 指定された難易度に基づいて数字を削除し、問題を作成
    # 'difficulty' は削除する数字の数を表す
    puzzle = remove_numbers_strict(complete_board, difficulty)
    
    # 最終的な数独の問題を返す
    return puzzle


# ナンプレの盤面を視覚的に表示するための関数
def print_sudoku(board):
    # 最初の区切り線を表示
    print("+-------+-------+-------+")
    
    # 各行をループして盤面を表示
    for i in range(9):
        row = "| "  # 行の先頭に区切りを追加
        for j in range(9):
            if board[i][j] == 0:
                # 空白セル(0のセル)は空白として表示
                row += "  "
            else:
                # セルの数字を文字列として表示
                row += str(board[i][j]) + " "
            
            # 3列ごとに区切りを追加
            if (j + 1) % 3 == 0:
                row += "| "
        
        # 現在の行を出力
        print(row)
        
        # 3行ごとに区切り線を表示
        if (i + 1) % 3 == 0:
            print("+-------+-------+-------+")


# ナンプレの盤面を解いて、その結果を表示するための関数
def solve_sudoku_and_print(board):
    # 内部関数 solve を定義して、数独の盤面を再帰的に解く
    def solve(board):
        # 盤面の全ての行をループ
        for row in range(9):
            # 各行の全ての列をループ
            for col in range(9):
                # 空白セル(0が格納されているセル)を見つけた場合
                if board[row][col] == 0:
                    # 1から9までの数字を順に試す
                    for num in range(1, 10):
                        # 数字がそのセルに配置できるかをチェック
                        if is_valid(board, row, col, num):
                            # 配置可能な場合、そのセルに数字を配置
                            board[row][col] = num
                            
                            # 再帰的に次のセルを埋めようとする
                            if solve(board):
                                return True
                            
                            # 再帰呼び出しが失敗した場合、配置を元に戻す(バックトラック)
                            board[row][col] = 0
                    
                    # 1から9のいずれの数字も配置できない場合、Falseを返してバックトラックを促す
                    return False
        
        # 全てのセルが適切に埋められた場合、Trueを返す
        return True

    # 盤面が解けた場合、解を表示
    if solve(board):
        print_sudoku(board)
    else:
        # 解けない場合、エラーメッセージを表示
        print("解けない")


# ユーザーが指定した難易度に基づいて数独の問題を自動的に生成し、その問題を解いて表示するための関数
# 自動化プロセスの関数
def sudoku_solver_process(level):
    # 難易度に応じた穴の数(空白セルの数)を設定
    if level == "初級":
        holes = 30  # 初級は30個の空白セル
    elif level == "中級":
        holes = 40  # 中級は40個の空白セル
    elif level == "上級":
        holes = 50  # 上級は50個の空白セル
    elif level == "難問":
        holes = 55  # 難問は55個の空白セル
    else:
        # 無効な難易度レベルが指定された場合のエラーメッセージ
        print("無効なレベルです。レベルは「初級」「中級」「上級」「難問」から選んでください。")
        return

    # 指定された難易度レベルのナンプレ問題を生成し表示
    print(f"\n{level}レベルのナンプレ問題:")
    puzzle = generate_sudoku_strict(holes)  # 数独の問題を生成
    print_sudoku(puzzle)  # 生成された問題を表示

    # 生成された問題の解答を解き、表示
    print(f"\n{level}レベルのナンプレ問題の解答:")
    solve_sudoku_and_print(puzzle)  # 問題を解き、解答を表示


# 先に定義した sudoku_solver_process 関数を使用して、「難問」レベルの数独の問題を生成し、その問題を解く例を示しています。この行は、プログラムの実行を開始するための具体的な呼び出し例。
# 例: 初級レベルのナンプレ問題を生成して解く
# "難問" レベルを指定して、数独の問題を生成し、解答を表示
sudoku_solver_process("初級")

Ver 0.51で作った、初級・中級・上級・難問

以下、Ver 0.51で作った、初級・中級・上級・難問です。

すべて、解は一意であることは検証済。

なお、解答は割愛します。

初級 002〜004

Screenshot
Screenshot
Screenshot

中級 002〜004

Screenshot
Screenshot
Screenshot

上級 002〜004

Screenshot
Screenshot
Screenshot

難問 002〜004

Screenshot
Screenshot
Screenshot

まとめ〜大問題発生?

これまで、ナンプレ問題を作成させて、解答してを繰り返して、検証してきました。

公開してきたのは、初級・中級・上級・難問を各4問ずつですが、実は、もっと検証しています。

で、今、一番の問題がコレ!

スポンサーリンク

  • どの問題も簡単すぎる!具体的には…
    • 初級・中級・上級にレベル差を感じない
    • どれも簡単すぎる
    • 簡単と言えば、特に、難問が「少しも難問」では無い!
  • ちなみに、難問設定を「空白60個」にしてみたが…
    • 空白60個では、解答が出力できない

つまり、今後の課題を、こう考えました。

  • 問題の難しさを左右するのは、空白個数だけではないみたい
  • それに変わるものは何?

この解決には、少し時間がかかりそう。

スポンサーリンク

よかったらシェアしてね!
  • URLをコピーしました!
目次