* Retranslate the complete Japanese online edition from Chinese * Fit Japanese homepage labels and localize heading permalinks * Point Japanese contribution instructions to the Japanese preview * Replace locale conditionals with multilingual UI tables
4.4 KiB
n クイーン問題
!!! question
チェスの規則では、クイーンは同じ行、列、斜めの線上にある駒を攻撃できます。$n$ 個のクイーンと $n \times n$ の盤面が与えられたとき、どのクイーン同士も攻撃できない配置を求めてください。
下図のように、n = 4 の場合は二つの解があります。バックトラッキングの観点では、n \times n の盤面の n^2 個のマスが、すべての選択肢 choices です。クイーンを一つずつ置くたびに盤面が変わり、その時々の盤面が状態 state になります。
下図に、三つの制約条件を示します。同じ行、同じ列、同じ対角線上に複数のクイーンを置いてはいけません。対角線には、主対角線方向 \ と副対角線方向 / の 2 種類があることに注意してください。
行ごとに配置する戦略
クイーンの個数と盤面の行数はどちらも n なので、各行にはクイーンをちょうど一つずつ置かなければならないことがわかります。
そこで、先頭行から最後の行まで、各行に一つずつ配置する戦略を取れます。
下図に、4 クイーン問題で行ごとに配置する過程を示します。図の大きさの都合で、先頭行の探索分岐は一つだけを展開し、列と対角線の制約を満たさない配置は枝刈りしています。
本質的には、行ごとの配置も枝刈りとして働き、同じ行に複数のクイーンを置くすべての探索分岐を避けています。
列と対角線による枝刈り
列の制約を満たすため、長さ n のブール配列 cols で、各列にクイーンがあるかを記録します。配置する前に cols を調べ、すでにクイーンがある列を枝刈りします。バックトラッキング中は、cols の状態を動的に更新します。
!!! tip
行列の原点は左上にあり、行インデックスは上から下へ、列インデックスは左から右へ増えることに注意してください。
では、対角線の制約はどう扱えばよいでしょうか。あるマスの行・列インデックスを (row, col) とします。主対角線方向の線を一つ選ぶと、その線上のすべてのマスでは行インデックスから列インデックスを引いた値が等しく、row - col が一定です。
つまり、二つのマスが row_1 - col_1 = row_2 - col_2 を満たせば、同じ主対角線方向の線上にあります。この性質を使い、下図の配列 diags1 に、各線上にクイーンがあるかを記録できます。
同様に、副対角線方向の線上では、すべてのマスの row + col が一定です。配列 diags2 を使って、この方向の制約も扱えます。
コードの実装
n 次の正方行列では、row - col の範囲は $[-n + 1, n - 1]$、row + col の範囲は [0, 2n - 2] です。したがって、どちらの方向の対角線も 2n - 1 本あり、配列 diags1 と diags2 の長さはいずれも 2n - 1 です。
[file]{n_queens}-[class]{}-[func]{n_queens}
行ごとの配置を n 回行います。列の制約を考慮すると、先頭行から最後の行までの選択肢は $n$、$n-1$、$\dots$、$2$、1 個なので、O(n!) 時間かかります。解を記録するときは行列 state をコピーして res に追加し、コピーには O(n^2) 時間かかります。したがって、全体の時間計算量は O(n! \cdot n^2) です。実際には、対角線の制約による枝刈りでも探索空間が大きく減るため、この計算量から想定されるより効率よく探索できることが多くなります。
配列 state は $O(n^2)$、配列 cols、diags1、diags2 はそれぞれ O(n) の空間を使います。最大再帰深度は n なので、スタックフレームに O(n) の空間を使います。したがって、空間計算量は O(n^2) です。