はじめに
数独(ナンプレ)は論理的思考を鍛えるのに最適なパズルです。
初心者でも「どのようにして 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を入れ替えて、ほぼランダムな解を得るnumpyのnp.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? (実際の数は...)
...
確認ポイント
- 生成時間 は数秒以内に収まり、実務でも負荷にならない
- 入力数 30 は「標準的な難易度」
- コマンドラインのみで完結
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_numbers の clues を 20 以内に設定すると「難易度が上がる」ようになります。 |
| ④ 日本語環境で動かない | ファイル冒頭に #!/usr/bin/env python3 と # -*- coding: utf-8 -*- を入れると安全。 |
| ⑤ ソルバーにヒントが欲しい | A、DPLL* などの手法を導入すれば、バックトラッキングより高速化。 |
8.まとめ ― 何を得られたか
- 再帰アルゴリズム(バックトラッキング)で簡易ソルバの作成
- 完全盤面 → 消去 → パズル という生成フローとその実装
- コマンドライン 上での動作検証コードと、簡易 GUI を付加した実用例
- 一意性判定 で「本当にオリジナルな問題」を提供するための高度手法
-
time,pprint,tkinterなどの標準モジュールで完結
これらを組み合わせれば、 初心者にとっても扱い易い小さな数独ジェネレータ が完成します。
さらに、学習やゲームアプリケーションへの拡張も容易なようにモジュール化してありますので、ぜひ自分のプロジェクトに取り込み、カスタマイズしてみてください。 🚀

コメント