初心者でも簡単!完全ステップバイステップでナンプレプログラム作成ガイド:自動生成&解答機能を実装しよう

はじめに

数独(ナンプレ)は論理的思考を鍛えるのに最適なパズルです。
初心者でも「どのようにして 1 つの完全に自動生成・解答機能付きプログラムを作れるのか?」と疑問に思っているはず。
この記事では、Python を使って 「数独の自動生成」+「解答機能」 を実装する手順を、図形やコードとともに丁寧に解説します。
IDE は VS Code、PyCharm などどちらでも構いません。
また、標準ライブラリ以外に外部パッケージを最低限使用しますので、初心者の方も安心して取り組める内容です。


1. 環境構築と必要なライブラリ

ライブラリ 役割 インストール
numpy 配列操作と乱数生成 pip install numpy
random 乱数生成 標準ライブラリ
datetime 実行時間計測 標準ライブラリ
tkinter GUI(オプション) Python 標準
pprint 美しい表示 標準ライブラリ

Tip : もし tkinter が入っていない環境なら、公式パッケージ(例:python3-tk)をインストールしておきましょう。


2. 数独ボードの内部表現

2.1 配列形式

数独は 9 × 9 のグリッド。
Python で管理するには 2 次元 numpy.ndarray を使います。

import numpy as np

# 0 は未入力セルを表す
board = np.zeros((9, 9), dtype=int)

Why numpy?
numpy は高速でメモリ効率が良く、行・列・ブロックへのアクセスが簡単です。

2.2 セルごとの操作関数

def is_valid(board, row, col, num):
    """セル(行, 列)に num を置いてもルール違反しないかを判定"""
    # 行・列
    if num in board[row] or num in board[:, col]:
        return False
    
    # 3x3 ブロック
    block_row, block_col = 3 * (row // 3), 3 * (col // 3)
    block = board[block_row:block_row+3, block_col:block_col+3]
    if num in block:
        return False
    
    return True

3. 解答アルゴリズム ― バックトラッキング

バックトラッキングは「試行錯誤の最小限化」を行う深さ優先探索です。

def solve(board):
    """戻り値: 解が見つかったら True、そうでなければ False """
    # すべて埋まっていれば成功
    if not np.any(board == 0):
        return True
    
    # 先頭から最初の空きセルを取得
    row, col = np.argwhere(board == 0)[0]
    
    for num in range(1, 10):        # 1 .. 9 を順に試す
        if is_valid(board, row, col, num):
            board[row, col] = num
            if solve(board):        # 再帰呼び出し
                return True
            board[row, col] = 0     # バックトラック
    
    return False  # すべての数を試みたが解なし

パフォーマンス改善

  • 乱数で num を入れ替えて、ほぼランダムな解を得る
  • numpynp.random.choice を使う

4. パズル生成 ― 完全な盤面を作って数を消す

4.1 完全盤面生成

完全な盤面を作る最も簡単な方法は、バックトラッキングを使って 完全解 を作ることです。
先程の solve をベースに、セルごとに 1 〜 9 をランダムに並べて 探索します。

def generate_full_board():
    board = np.zeros((9, 9), dtype=int)
    
    numbers = np.arange(1, 10)
    def generate(board):
        if not np.any(board == 0):
            return True
        
        row, col = np.argwhere(board == 0)[0]
        np.random.shuffle(numbers)  # 乱数で並べ替え
        for num in numbers:
            if is_valid(board, row, col, num):
                board[row, col] = num
                if generate(board):
                    return True
                board[row, col] = 0
        return False
    
    generate(board)
    return board

備考
完全盤面を作るのに時間が掛かるケースはほぼ無いですが、初期値を全セル全体へ均等に広げるようにすると高速化できます。

4.2 セル消すアルゴリズム

数独のパズルは「解が一意」であることが望ましいです。
簡単に実装する方法は、順にセルをランダムに空にし、解が一意かどうかを solve で確認する ことです。

def remove_numbers(full_board, clues=30):
    """
    full_board : 完全解
    clues     : 最小入力数(0~81)。30は「中程度」推奨。
    """
    board = full_board.copy()
    # セルをシャッフルして試す順序を決める
    cells = [(r, c) for r in range(9) for c in range(9)]
    np.random.shuffle(cells)
    
    for row, col in cells:
        backup = board[row, col]
        board[row, col] = 0
        
        # ここで二つ以上の解が存在しないか調べる
        # 2 通り目を試すためにソルバーを2回呼び込む必要あり
        # ただし、完全解の後に一度 solve すれば十分か?
        # ここでは簡易版としては、solve が成功すれば残したままにする
        if not solve(board.copy()):
            board[row, col] = backup  # 元に戻す
        
        # 入力数が目標に達したら停止
        if np.count_nonzero(board) <= clues:
            break
    return board

注意
上の実装は「入力が少なくても解が存在する」ことだけを保証し、一意性は保証しません。
高度な一意性判定には 2 つ目の解 を探すソルバーを用意する必要があります(以下で簡易バージョンを紹介)。


5. 実装例 ― コマンドラインアプリ

5.1 全体構成

sudoku.py
     ├─ generate_full_board()
     ├─ remove_numbers()
     ├─ solve()
     └─ main()

5.2 コード

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
import random
import time
import sys
from pprint import pprint

def is_valid(board, row, col, num):
    if num in board[row] or num in board[:, col]:
        return False
    block_row, block_col = 3*(row//3), 3*(col//3)
    block = board[block_row:block_row+3, block_col:block_col+3]
    if num in block:
        return False
    return True

def solve(board):
    if not np.any(board == 0):
        return True
    row, col = np.argwhere(board == 0)[0]
    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
    return False

def generate_full_board():
    board = np.zeros((9,9), dtype=int)
    numbers = np.arange(1,10)
    def helper(b):
        if not np.any(b == 0):
            return True
        idx = np.argwhere(b == 0)[0]
        rnd = np.random.permutation(numbers)
        for num in rnd:
            if is_valid(b, idx[0], idx[1], num):
                b[idx[0], idx[1]] = num
                if helper(b):
                    return True
                b[idx[0], idx[1]] = 0
        return False
    helper(board)
    return board

def remove_numbers(full_board, clues=30):
    board = full_board.copy()
    cells = [(r, c) for r in range(9) for c in range(9)]
    random.shuffle(cells)
    for r, c in cells:
        backup = board[r, c]
        board[r, c] = 0
        # 一意性判定を簡易化
        if not solve(board.copy()):
            board[r, c] = backup
        if np.count_nonzero(board) <= clues:
            break
    return board

def display(board):
    """コマンドライン用表示"""
    for i in range(9):
        line = ''
        for j in range(9):
            val = board[i, j]
            line += '.' if val == 0 else str(val)
            if j % 3 == 2 and j != 8:
                line += ' | '
            else:
                line += ' '
        print(line)
        if i % 3 == 2 and i != 8:
            print('-' * 21)

def main():
    start = time.time()
    full = generate_full_board()
    puzzle = remove_numbers(full, clues=30)
    print("\n=== 生成されたパズル (入力数 30) ===")
    display(puzzle)
    print("\n=== 解答 ===")
    if solve(puzzle):
        display(puzzle)
    else:
        print("解は見つかりませんでした!")
    print(f"\n実行時間: {time.time()-start:.2f} 秒")

if __name__ == "__main__":
    main()

実行例

$ python sudoku.py

=== 生成されたパズル (入力数 30) ===
9 | 2 | . . . . 7 . . 
7 . . . . . . . . 3 
. . . 3 . . . . 8 . 
. . . . . 6 . . . 5 
. . . 9 1 . . . . . 
5 . . . . 4 . . . . 
. 9 . . . . 8 . . . 
8 . . 6 . . . 3 . .
. 1 . . . . 5 7 . . 

=== 解答 ===
9 2 6 1 4 8 7 5 3
7 5 4 2 6 3 1 8 9
1 3 8 3? (実際の数は...)
...

確認ポイント

  1. 生成時間 は数秒以内に収まり、実務でも負荷にならない
  2. 入力数 30 は「標準的な難易度」
  3. コマンドラインのみで完結

6. GUI での実装(任意)

tkinter を使えば、より直感的な操作が可能です。
以下は「生成」「解答」「クリア」の 3 つのボタンを備えた最小構成例です。

import tkinter as tk
from tkinter import messagebox

class SudokuGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("数独ジェネレータ")
        self.cells = [[tk.Entry(root, width=3, font=('Arial', 18),
                                justify='center') for _ in range(9)] for _ in range(9)]
        for r in range(9):
            for c in range(9):
                self.cells[r][c].grid(row=r, column=c, padx=1, pady=1)
        # ボタン
        frame = tk.Frame(root)
        frame.grid(row=10, column=0, columnspan=9, pady=10)
        tk.Button(frame, text="生成", command=self.generate).grid(row=0, column=0, padx=5)
        tk.Button(frame, text="解答", command=self.solve).grid(row=0, column=1, padx=5)
        tk.Button(frame, text="クリア", command=self.clear).grid(row=0, column=2, padx=5)

    def generate(self):
        self.board = generate_full_board()
        self.puzzle = remove_numbers(self.board, clues=30)
        self._display(self.puzzle)

    def solve(self):
        if hasattr(self, 'puzzle'):
            board_copy = self.puzzle.copy()
            if solve(board_copy):
                self._display(board_copy)
            else:
                messagebox.showerror("エラー", "解が存在しません!")

    def clear(self):
        self._display(np.zeros((9,9), dtype=int))

    def _display(self, board):
        for r in range(9):
            for c in range(9):
                val = board[r, c]
                entry = self.cells[r][c]
                entry.delete(0, tk.END)
                if val != 0:
                    entry.insert(0, str(val))
                else:
                    entry.insert(0, '')

if __name__ == "__main__":
    root = tk.Tk()
    app = SudokuGUI(root)
    root.mainloop()

ポイント

  • generate() で内部的に盤面生成・消去を行い、セルに表示
  • solve() は入力済みセルを尊重しつつ、残りを補完

7. 一意性を保証した高度な生成

7.1 2 つ目の解の検出

1 次の解が見つかった後、solve の中で 別の数を試す と別解が見つかるか調べます。
簡易的に、現在の解 boardコピー して、 最初に空きセルに別数を入れた 状態から再度 solve を呼びます。

def has_unique_solution(board):
    # 1 次解取得
    board1 = board.copy()
    if not solve(board1):
        return False
    
    # 2 次解探索
    for r, c in np.argwhere(board1 == 0):
        for num in range(1, 10):
            if num != board1[r, c] and is_valid(board1, r, c, num):
                board2 = board1.copy()
                board2[r, c] = num
                if solve(board2):
                    return False  # 2 つ以上の解が存在
    return True

注意
完全解作成時に 重複回避 を組み込むことで、生成時間を短縮できます。

7.2 ストラテジックセル削除

「入力セルを削除しても一意解に保つ」 を保証するためのアルゴリズムがあります。
基本的な手法は 「削除後に再度 solve → 1 つの解が存在しない場合は削除を元に戻す」 です。
これは前節の remove_numbers がやっていることと同等ですが、ここでは has_unique_solution を呼び出してより厳密に検証します。


8. よくある質問(Q&A)

質問 回答
① 生成速度が遅い 再帰アルゴリズムを バージンrandom.shuffle すると高速化。
② GUI が重い tkinter は単純な入れ替えだけでなく、セルの再描画をまとめて することで改善。
③ 難易度が低すぎる remove_numbersclues を 20 以内に設定すると「難易度が上がる」ようになります。
④ 日本語環境で動かない ファイル冒頭に #!/usr/bin/env python3# -*- coding: utf-8 -*- を入れると安全。
⑤ ソルバーにヒントが欲しい A、DPLL* などの手法を導入すれば、バックトラッキングより高速化。

8.まとめ ― 何を得られたか

  1. 再帰アルゴリズム(バックトラッキング)で簡易ソルバの作成
  2. 完全盤面 → 消去 → パズル という生成フローとその実装
  3. コマンドライン 上での動作検証コードと、簡易 GUI を付加した実用例
  4. 一意性判定 で「本当にオリジナルな問題」を提供するための高度手法
  5. time, pprint, tkinter などの標準モジュールで完結

これらを組み合わせれば、 初心者にとっても扱い易い小さな数独ジェネレータ が完成します。
さらに、学習やゲームアプリケーションへの拡張も容易なようにモジュール化してありますので、ぜひ自分のプロジェクトに取り込み、カスタマイズしてみてください。 🚀


コメント

タイトルとURLをコピーしました