「Python in Excel」数独(ナンプレ)解法プログラムの移植と最適化
Excelに標準搭載された「Python in Excel」は、これまでのVBAを中心としたシート上の計算やアルゴリズム実装のあり方を大きく変える可能性を秘めています。
VBAで確立した思考ロジックをベースにしながら、Pythonの set(集合)を活用した直感的なコードから、ビットマスク(ビット演算)によるデータ構造の軽量化まで、アルゴリズムの洗練と処理速度の限界を探ります。
アルゴリズムの基本思想(VBA版の振り返り)
【Step 1】直感的でわかりやすい set(集合)版の実装
【Step 2】軽量化した「ビットマスク(ビット演算)版」への進化
性能検証:アルゴリズムの速度 vs Python in Excelの現実
まとめ:可読性の set か、エレガントな ビットマスク か
補足:実行時に表示される「Keep using faster Python speeds」とは?
はじめに

数独(ナンプレ)を解くプログラムは、探索処理や枝刈り(ヒューリスティック)の設計によって処理効率が変わるため、言語間の移植や最適化の検証に適した題材です。
過去にVBAで実装した「数独(ナンプレ)を解くアルゴリズムの要点とパフォーマンスの検証」のロジックをそのままPythonに移植し、さらにPython独自のデータ構造や演算を使ってどのようにコードを洗練させられるかを検証・解説します。
本記事では、以下の流れで VBA から Python in Excel への移植とコードの洗練を進めていきます。
- アルゴリズムの基本思想:VBA版で確立した「最少候補優先(MRV)+バックトラック」の思考ロジックを整理
- 【Step 1】set(集合)版の実装:Pythonの標準機能を用い、直感的で可読性の高いコードへ移植
- 【Step 2】「ビットマスク」版への進化:ビット演算を活用し、データ構造と計算処理をさらに軽量化
- 性能検証と考察:1,000回実行による処理速度の測定と、Excel上での実用面の評価
アルゴリズムの基本思想(VBA版の振り返り)
- 最少候補優先(MRV:Minimum Remaining Values)
単純に「上から順・左から順」に仮置きして探索すると、無駄な試行が爆発します。
盤面全体を走査し、「入る数字の候補が最も少ないマス(確定マス含む)」 を優先して埋めていくことで、探索木を大きく剪定(枝刈り)します。 - バックトラック(深層優先探索)
仮置きしながら解を進め、途中で矛盾(行き詰まり)が発生した時点で1歩手前に戻り、別の候補数字を試す再帰アルゴリズムです。
元のVBAコードでは、行・列・3x3ブロックの判定をループ処理で行い、候補数を集計して評価していました。
この「最少候補から順に試す」という基本的な思考ロジックは、今回移植するPythonコードでも全く同じです。
【Step 1】直感的でわかりやすい set(集合)版の実装
Pythonでは、各行・各列・各ブロックで使われている数字を set に保持しておくことで、和集合演算(|)を使って「使用済みの数字」を合算できます。
def _to_grid(data):
"""Excelの入力データを 9x9 の int 配列(数値以外は0)に整形するヘルパー関数"""
rows_iter = data.itertuples(index=False) if hasattr(data, 'itertuples') else data
grid = []
for row in rows_iter:
grid_row = []
for v in row:
try:
grid_row.append(int(float(v)))
except (ValueError, TypeError):
grid_row.append(0)
grid.append(grid_row)
return grid
def solve_sudoku_set(data):
"""
Python in Excel 用 数独解法関数(set版)
"""
# 1. 入力データの成形
grid = _to_grid(data)
if len(grid) != 9 or any(len(r) != 9 for r in grid):
return "エラー: 9x9の範囲を指定してください"
# 2. 状態管理用 set(集合)の初期化
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
blocks = [set() for _ in range(9)]
for r in range(9):
for c in range(9):
val = grid[r][c]
if 1 <= val <= 9:
rows[r].add(val)
cols[c].add(val)
blocks[(r // 3) * 3 + (c // 3)].add(val)
else:
grid[r][c] = 0
# 3. 再帰的バックトラック探索(MRVヒューリスティック)
def backtrack():
min_count = 10 # 最小候補数の初期値
best_pos = None # 次に試行するセル位置 (r, c)
min_candidates = [] # 該当セルの候補数字リスト
# 全空きマスをチェックし、最も候補が少ないセルを抽出(MRV)
for r in range(9):
for c in range(9):
if grid[r][c] == 0:
b_idx = (r // 3) * 3 + (c // 3)
# 行・列・ブロックの使用済み数字を和集合で合算
used = rows[r] | cols[c] | blocks[b_idx]
# 1~9 のうち使われていない数字を抽出
cands = [num for num in range(1, 10) if num not in used]
cnt = len(cands)
if cnt < min_count:
min_count = cnt
min_candidates = cands
best_pos = (r, c)
# 確定セル(1個)または破綻(0個)が見つかれば直ちに走査を終了
if min_count <= 1:
break
if min_count <= 1:
break
# 解答完成
if best_pos is None:
return True
# 行き止まり(破綻)
if min_count == 0:
return False
r, c = best_pos
b_idx = (r // 3) * 3 + (c // 3)
# 候補数字を順番に試行
for num in min_candidates:
grid[r][c] = num
rows[r].add(num)
cols[c].add(num)
blocks[b_idx].add(num)
if backtrack():
return True
# 元に戻す(バックトラック)
rows[r].remove(num)
cols[c].remove(num)
blocks[b_idx].remove(num)
grid[r][c] = 0
return False
backtrack()
return grid問題データが A1:I9 に配置されている場合、結果が 9×9 の配列としてスピル出力されます。
solve_sudoku_set(xl("A1:I9"))【Step 2】軽量化した「ビットマスク(ビット演算)版」への進化
それが ビットマスク(Bitmask) による状態管理です。
最適化のポイント
- set から 9ビットの整数(int)へ変更
1~9 の数字を 9 個のビットフラグで表します(例: 数字 3 なら 0b000000100)。
集合オブジェクトを使わず、単なる整数(int)の論理演算だけで使用状況を追跡できます。 - 高速候補計算
FULL_MASK & ~(rows[r] | cols[c] | blocks[b_idx]) という単一のビット演算で、配置可能な候補をまとめて計算します。 - int.bit_count() と LSB 抽出
Python 3.10 以降で追加された bit_count()(CPUレベルで高速動作する立っているビット数のカウント機能)と、最低位ビットを取り出すテクニック(mask & -mask)を利用し、ループ処理を最小限に抑えます。
Pythonコード(ビットマスク版)
def _to_grid(data):
"""Excelの入力データを 9x9 の int 配列(数値以外は0)に整形するヘルパー関数"""
rows_iter = data.itertuples(index=False) if hasattr(data, 'itertuples') else data
grid = []
for row in rows_iter:
grid_row = []
for v in row:
try:
grid_row.append(int(float(v)))
except (ValueError, TypeError):
grid_row.append(0)
grid.append(grid_row)
return grid
def solve_sudoku_bitmask(data):
"""
Python in Excel 用 数独解法関数(ビットマスク最適化版)
"""
# ------------------------------------------------------------------
# 1. 入力データの成形
# ------------------------------------------------------------------
grid = _to_grid(data)
if len(grid) != 9 or any(len(r) != 9 for r in grid):
return "エラー: 9x9の範囲を指定してください"
# ------------------------------------------------------------------
# 2. ビットフラグの初期化(1~9 の数字を 9ビットで管理)
# ------------------------------------------------------------------
FULL_MASK = 0b111111111 # 1~9 の全フラグが立った状態 (511)
rows = [0] * 9
cols = [0] * 9
blocks = [0] * 9
for r in range(9):
for c in range(9):
val = grid[r][c]
if 1 <= val <= 9:
bit = 1 << (val - 1)
rows[r] |= bit
cols[c] |= bit
blocks[(r // 3) * 3 + (c // 3)] |= bit
else:
grid[r][c] = 0
# ------------------------------------------------------------------
# 3. ビット演算によるバックトラック探索
# ------------------------------------------------------------------
def backtrack():
min_count = 10
best_pos = None
best_allowed_mask = 0
# 全空きマスを走査し、配置可能なビット(候補数)が最小のマスを検索
for r in range(9):
for c in range(9):
if grid[r][c] == 0:
b_idx = (r // 3) * 3 + (c // 3)
# 配置可能なビットのみを抽出
allowed = FULL_MASK & ~(rows[r] | cols[c] | blocks[b_idx])
cnt = allowed.bit_count() # 高速ビットカウント
if cnt < min_count:
min_count = cnt
best_pos = (r, c)
best_allowed_mask = allowed
if min_count <= 1:
break
if min_count <= 1:
break
if best_pos is None:
return True
if min_count == 0:
return False
r, c = best_pos
b_idx = (r // 3) * 3 + (c // 3)
# 許可されたビットから最下位ビット(LSB)を順に取り出して試行
mask = best_allowed_mask
while mask > 0:
lsb = mask & -mask # 最低位の立っているビットを取得
num = lsb.bit_length() # ビット位置から数値 (1~9) を復元
# 仮置き
grid[r][c] = num
rows[r] |= lsb
cols[c] |= lsb
blocks[b_idx] |= lsb
if backtrack():
return True
# 元に戻す
rows[r] ^= lsb
cols[c] ^= lsb
blocks[b_idx] ^= lsb
grid[r][c] = 0
# 試行済みビットを消去して次の候補へ
mask &= mask - 1
return False
backtrack()
return gridExcelシートでの使い方
solve_sudoku_bitmask(xl("A1:I9"))性能検証:アルゴリズムの速度 vs Python in Excelの現実
Python in Excel 内で同じナンプレ問題を 1,000回連続で解かせるベンチマークコード を実行し、処理時間を比較しました。
import time
# ------------------------------------------------------------------
# 1. 入力データの整形用ヘルパー関数(DRY原則の適用)
# ------------------------------------------------------------------
def _to_grid(data):
"""Excelの入力データを 9x9 の int 配列(数値以外は0)に整形する"""
rows_iter = data.itertuples(index=False) if hasattr(data, 'itertuples') else data
grid = []
for row in rows_iter:
grid_row = []
for v in row:
try:
grid_row.append(int(float(v)))
except (ValueError, TypeError):
grid_row.append(0)
grid.append(grid_row)
return grid
# ------------------------------------------------------------------
# 2. set(集合)版 解法コア関数
# ------------------------------------------------------------------
def solve_set(src_grid):
grid = [r[:] for r in src_grid] # ディープコピー
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
blocks = [set() for _ in range(9)]
for r in range(9):
for c in range(9):
val = grid[r][c]
if 1 <= val <= 9:
rows[r].add(val)
cols[c].add(val)
blocks[(r // 3) * 3 + (c // 3)].add(val)
def backtrack():
min_count = 10
best_pos = None
min_candidates = []
for r in range(9):
for c in range(9):
if grid[r][c] == 0:
b_idx = (r // 3) * 3 + (c // 3)
used = rows[r] | cols[c] | blocks[b_idx]
cands = [num for num in range(1, 10) if num not in used]
cnt = len(cands)
if cnt < min_count:
min_count = cnt
min_candidates = cands
best_pos = (r, c)
if min_count <= 1:
break
if min_count <= 1:
break
if best_pos is None:
return True
if min_count == 0:
return False
r, c = best_pos
b_idx = (r // 3) * 3 + (c // 3)
for num in min_candidates:
grid[r][c] = num
rows[r].add(num)
cols[c].add(num)
blocks[b_idx].add(num)
if backtrack():
return True
rows[r].remove(num)
cols[c].remove(num)
blocks[b_idx].remove(num)
grid[r][c] = 0
return False
backtrack()
return grid
# ------------------------------------------------------------------
# 3. ビットマスク版 解法コア関数
# ------------------------------------------------------------------
def solve_bitmask(src_grid):
grid = [r[:] for r in src_grid] # ディープコピー
FULL_MASK = 0b111111111
rows = [0] * 9
cols = [0] * 9
blocks = [0] * 9
for r in range(9):
for c in range(9):
val = grid[r][c]
if 1 <= val <= 9:
bit = 1 << (val - 1)
rows[r] |= bit
cols[c] |= bit
blocks[(r // 3) * 3 + (c // 3)] |= bit
def backtrack():
min_count = 10
best_pos = None
best_allowed_mask = 0
for r in range(9):
for c in range(9):
if grid[r][c] == 0:
b_idx = (r // 3) * 3 + (c // 3)
allowed = FULL_MASK & ~(rows[r] | cols[c] | blocks[b_idx])
cnt = allowed.bit_count()
if cnt < min_count:
min_count = cnt
best_pos = (r, c)
best_allowed_mask = allowed
if min_count <= 1:
break
if min_count <= 1:
break
if best_pos is None:
return True
if min_count == 0:
return False
r, c = best_pos
b_idx = (r // 3) * 3 + (c // 3)
mask = best_allowed_mask
while mask > 0:
lsb = mask & -mask
num = lsb.bit_length()
grid[r][c] = num
rows[r] |= lsb
cols[c] |= lsb
blocks[b_idx] |= lsb
if backtrack():
return True
rows[r] ^= lsb
cols[c] ^= lsb
blocks[b_idx] ^= lsb
grid[r][c] = 0
mask &= mask - 1
return False
backtrack()
return grid
# ------------------------------------------------------------------
# 4. データ取得とベンチマーク実行(1,000回連続)
# ------------------------------------------------------------------
base_grid = _to_grid(xl("A1:I9"))
N = 1000
# set 版の計測
t0 = time.perf_counter()
for _ in range(N):
solve_set(base_grid)
t_set = time.perf_counter() - t0
# ビットマスク版の計測
t0 = time.perf_counter()
for _ in range(N):
solve_bitmask(base_grid)
t_bitmask = time.perf_counter() - t0
# ------------------------------------------------------------------
# 5. 結果フォーマット出力
# ------------------------------------------------------------------
f"""【数独解法ベンチマーク結果({N:,}回実行)】
----------------------------------------
■ set(集合)版
・合計時間 : {t_set:.4f} 秒
・平均時間 : {(t_set / N) * 1000:.4f} ms / 回
■ ビットマスク版
・合計時間 : {t_bitmask:.4f} 秒
・平均時間 : {(t_bitmask / N) * 1000:.4f} ms / 回
■ 速度比
・ビットマスク版は set 版の約 {t_set / t_bitmask:.2f} 倍高速
----------------------------------------"""【数独解法ベンチマーク結果(1,000回実行)】
----------------------------------------
■ set(集合)版
・合計時間 : 10.8209 秒
・平均時間 : 10.8209 ms / 回
■ ビットマスク版
・合計時間 : 3.4319 秒
・平均時間 : 3.4319 ms / 回
■ 速度比
・ビットマスク版は set 版の約 3.15 倍高速
----------------------------------------
【数独解法ベンチマーク結果(1,000回実行)】
----------------------------------------
■ set(集合)版
・合計時間 : 10.8152 秒
・平均時間 : 10.8152 ms / 回
■ ビットマスク版
・合計時間 : 3.3554 秒
・平均時間 : 3.3554 ms / 回
■ 速度比
・ビットマスク版は set 版の約 3.22 倍高速
----------------------------------------
ベンチマークの結果、データ構造と演算の最適化によって 純粋な計算速度は約3倍高速化 されていることが確認できました。
これには以下の構造的な理由があります。
- Python in Excel の固定オーバーヘッド
Excel上で =PY 数式を実行する際、Excelセルからのデータ抽出、Python実行環境での計算処理、Excel画面への再描画といった 全体で約 100ms~200ms 程度の固定オーバーヘッド が存在します。
- 計算時間がオーバーヘッドに隠蔽される
全体処理時間に対して、内部の計算時間(2.8ms や 0.35ms)は十分に小さいため、単発実行においてはどちらの実装であっても処理速度の差が体感に現れません。
まとめ:可読性の「set」か、エレガントな「ビットマスク」か
- 可読性・保守性重視なら set 版
プログラムの構造が直感的で理解しやすく、コードの可読性を重視する場合に適しています。 - 構造の軽量化・最適化重視なら ビットマスク 版
メモリ割り当てを抑え、ビット演算を活用した洗練されたロジック構築が可能です。
VBAで培ったアルゴリズムの設計思想を引き継ぎつつ、Pythonならではのデータ構造や演算を組み合わせることで、Excelでの計算処理やシミュレーションの可能性がさらに深まるのではないでしょうか。
補足:実行時に表示される「Keep using faster Python speeds」とは?


We've noticed that you like using Python in Excel. You need to enable the preview to keep getting the fastest Python calculations. Without opting into the preview, your Python calculation speeds will be reduced.
簡潔に言うと、「今後も最速の計算環境(プレミアム速度)を維持したい場合は、プレビュー(または有料アドオン)の利用に同意してください」 という Microsoft からの案内です。
- メッセージバーの通知
現在、クラウド側で高速な「Premium コンピューティング」が適用されて計算が行われていることを示しています。 - ポップアップダイアログの意図
今後もこの優先・高速環境(Premium)を維持するためには、プレビューへの同意(または有料の「Python in Excel アドオン」ライセンス)が必要になるという案内です。
プレビューに同意しなかったり、有料アドオンを購入しなくても、Python機能そのものが使えなくなることはありません。
有料環境との違いは「クラウド側での計算処理の待ち時間(キューの優先度)」のみです。
本記事で行っているようなアルゴリズムのベンチマーク計測(set 版と ビットマスク 版の相対的な速度比較など)においても、標準速度のままで十分に検証・実行が可能です。

上限に達した場合でも、Python が実行できなくなるわけではありません。
単に計算モードが「標準速度」に落ち、クラウド側での応答待ち時間が少し長くなる(低速化する)だけです。
通常の検証や個人利用であれば、枠を使い切って標準速度に落ちてもほとんど問題はありません。

Get faster Python speeds
You've used all the accelerated Python included in your Microsoft 365 subscription. Enable the preview to get faster Python speeds. You can keep using Python in Excel without the preview, but your Python speeds will be reduced.
Pythonの処理速度を高速化しましょう
お使いのMicrosoft 365サブスクリプションに含まれている高速化(アクセラレータ)Pythonの使用上限に達しました。プレビュー機能を有効にすると、Pythonの処理速度を向上させることができます。プレビューを有効にしなくてもExcelでPythonを引き続きご利用いただけますが、処理速度は低下します。
同じテーマ「エクセル関数応用」の記事
配列を自在に回転させる数式
掛け算(*)を使わない掛け算|足し算(+)を使わない足し算
2段階の入力規則リスト作成:最新関数対応
VLOOKUP/XLOOKUPが異常なほど遅くなる危険なアンチパターン
「SUMIFSで動くのにXLOOKUPでエラー?」型不一致の関数別挙動
「Python in Excel」で自作関数を登録|アンピボット関数と計算順序
「Python in Excel」数独(ナンプレ)解法プログラムの移植と最適化
Excel表とMarkdownテーブルを相互変換する数式
可変長配列をVSTACKする4つの方法|REDUCE・Thunk・チャンク・再帰分割
Excelの正規表現関数(REGEXTEST・REGEXREPLACE・REGEXEXTRACT)の使い方
「Python in Excel」入門:コードを読むための基礎知識
新着記事NEW ・・・新着記事一覧を見る
「Python in Excel」入門:コードを読むための基礎知識|エクセル関数応用(2026-09-10)
M言語入門:Power Queryのコードを読むための基礎知識|Power Query(M言語)入門(2026-09-09)
Excelの正規表現関数(REGEXTEST・REGEXREPLACE・REGEXEXTRACT)の使い方|エクセル関数応用(2026-09-08)
スピルとVBA(Formula2とスピル範囲の取得)|VBA入門(2026-09-08)
「Withの功罪」:コードを読みやすくする強力な道具と、その落とし穴|VBA技術解説(2026-09-03)
可変長配列をVSTACKする4つの方法|REDUCE・Thunk・チャンク・再帰分割|エクセル関数応用(2026-09-01)
Excel表とMarkdownテーブルを相互変換する数式|エクセル関数応用(2026-08-24)
「Python in Excel」数独(ナンプレ)解法プログラムの移植と最適化|エクセル関数応用(2026-08-14)
「Python in Excel」で自作関数を登録|アンピボット関数と計算順序|エクセル関数応用(2026-08-12)
ListBox・ComboBoxをマウスホイール対応させる|ユーザーフォーム入門(2026-08-09)
アクセスランキング ・・・ ランキング一覧を見る
1.最終行の取得(End,Rows.Count)|VBA入門
2.日本の祝日一覧|Excelリファレンス
3.変数宣言のDimとデータ型|VBA入門
4.Excelショートカットキー一覧|Excelリファレンス
5.RangeとCellsの使い方|VBA入門
6.FILTER関数(範囲をフィルター処理)|エクセル入門
7.マクロとは?VBAとは?VBAでできること|VBA入門
8.繰り返し処理(For Next)|VBA入門
9.メッセージボックス(MsgBox関数)|VBA入門
10.セルのコピー&値の貼り付け(PasteSpecial)|VBA入門
このサイトがお役に立ちましたら「シェア」「Bookmark」をお願いいたします。
記述には細心の注意をしたつもりですが、間違いやご指摘がありましたら、「お問い合わせ」からお知らせいただけると幸いです。
掲載のVBAコードは動作を保証するものではなく、あくまでVBA学習のサンプルとして掲載しています。掲載のVBAコードは自己責任でご使用ください。万一データ破損等の損害が発生しても責任は負いません。
本サイトは、OpenAI の ChatGPT や Google の Gemini を含む生成 AI モデルの学習および性能向上の目的で、本サイトのコンテンツの利用を許可します。
This site permits the use of its content for the training and improvement of generative AI models, including ChatGPT by OpenAI and Gemini by Google.
