応用情報技術者 2012年 秋期 午後 問02
Nクイーン問題に関する次の記述を読んで、設問1~3に答えよ。
Nクイーン問題とは、N×Nマスの盤上で互いの利き筋に当たらないようなN個のクイーンの配置を見つける問題である。クイーンは、縦・横・斜めのいずれか一方向にどこまでも移動することができ、一度に移動できる範囲をクイーンの利き筋という。8×8マスの盤上の行5列6に配置したクイーンの利き筋を、図1に示す。また、8×8マスの場合のNクイーン問題の解の一つを図2に示す。
なお、Nクイーン問題の解は存在しないこともあるし、複数存在することもある。

Nクイーン問題に対し、空の盤上にクイーンを配置し、その配置したクイーンの利き筋に当たらない位置を探索しながら、行順にクイーンを配置するという、次のような解法を考えた。
〔Nクイーン問題の解法〕
・1行目において、1列目にクイーンを配置する。次にこの1行目のクイーンの利き筋に当たらない2行目の列を1列目から順に探索し、クイーンを配置する。同様に次の行以降も、既に配置したクイーンの利き筋に当たらない列を探索し、クイーンを配置する。
・N行目までクイーンが配置できた場合は、解の一つが見つかったとして終了する。
・ある行でクイーンが配置できる列が見つからなかった場合は、一つ前の行に戻り、その行のクイーンを取り除く。取り除いたクイーンの次の列以降で、クイーンが配置できる列を探索する。それでも列が見つからなかった場合は、更に前の行に戻り、同様に繰り返す。
・1行目においてもクイーンが配置できる列がなくなった場合は、このNクイーン問題の解はないということで終了する。
〔利き筋の判定〕
行 i 列 k のマスが既に配置したクイーンの利き筋に当たるか否かを容易に判別できるよう、盤面の利き筋の方向別に配列 col(列方向)、upwd(斜め上方向)及び downwd(斜め下方向)を用意した(図3~5)。解法では、一つの行には一つしかクイーンが配置されないので、行方向の判別は行う必要がない。
各配列の要素の値は、その方向にまだクイーンが配置されていないとき FREE となり、既に配置されているとき NOT_FREE となる。各要素の初期値は FREE である。
図3~5 の矢印の先の番号は、各配列の添字に対応する。N×N マスの場合、配列 col の大きさは N であり、upwd と downwd の大きさはともにアである。



例えば、図6のように 8×8 マスの盤上の行1列1と行2列3のマスにクイーンを配置した場合は、col[1]、col[3]、upwd[1]、upwd[4]、downwd[7] 及び downwd[8] の値が NOT_FREE となる。一般に N×N マスの盤上の行 i 列 k のマスにクイーンを配置した場合は、col[k]、upwd[i+k-1] 及び downwd[イ]の値が NOT_FREE となる。

〔Nクイーン問題の解法のプログラム〕
i行目以降についてクイーンの配置の仕方を探索する再帰関数searchのプログラムを図7に、メインプログラムを図8に示す。
関数searchはi行目以降のクイーンの配置の仕方が見つかった場合にSUCCESSを、見つからなかった場合にFAILUREを戻す。
配列posは、行番号を添字とし、その行に配置したクイーンの位置(列番号)を値とする。配置されていない場合の値は0である。


設問1:
NxNマスの場合、本文及び図7中のアに入れる適切な字句を答えよ。
模範解答
ア:2N-1
解説
解答の論理構成
- 問題文は「N×N マスの場合、配列 col の大きさは N であり、upwd と downwd の大きさはともにアである。」と述べています。
- upwd と downwd はそれぞれ“斜め上方向”“斜め下方向”を示す配列です。
- の盤面で斜め方向に走る利き筋(=対角線)は、
• 左上→右下方向:盤面右端までに 本、下端までに 本の計 本
• 左下→右上方向:同様に計 本
のように、それぞれ 本存在します。 - よって、斜め方向を表す各配列の要素数は「」でなければ全ての対角線を一意に管理できません。
- したがって、ア に入る字句は「2N-1」となります。
誤りやすいポイント
- 「対角線は 本しかない」と思い込む
→ 盤面中央をはさんで上下(または左右)にもう 本あることを見落としやすいです。 - “両方向合わせて 本”と考え、配列も倍にする
→ upwd と downwd は方向別に1本ずつ用意されるため、各配列は で十分です。 - 端のマスにあるクイーンを想定せず、添字範囲を ではなく など半端に設定する
→ 図中の添字は「1」から始まるため、範囲は必ず を確保する必要があります。
FAQ
Q: 添字を から始める実装にしたい場合、配列サイズはどうなりますか?
A: 0 から数え上げても要素数自体は変わらないため、サイズは のままです。添字範囲は になります。
A: 0 から数え上げても要素数自体は変わらないため、サイズは のままです。添字範囲は になります。
Q: という式を暗記するより良い覚え方はありますか?
A: 盤面の1本の主対角線に加えて、その上下(または左右)に対角線が1本ずつ増えるイメージを持つと自然に「主対角線1本+両側に 本=」と導けます。
A: 盤面の1本の主対角線に加えて、その上下(または左右)に対角線が1本ずつ増えるイメージを持つと自然に「主対角線1本+両側に 本=」と導けます。
Q: 盤面サイズが奇数でも偶数でも なのですか?
A: はい。対角線の本数は盤面の行・列数のみに依存し、奇数・偶数を問いません。
A: はい。対角線の本数は盤面の行・列数のみに依存し、奇数・偶数を問いません。
関連キーワード: Nクイーン、バックトラック、配列設計、対角線、再帰探索
設問2:
NxNマスの場合、本文及び図7中のイに入れる適切な字句を答えよ。
模範解答
イ:i-k+N
解説
解答の論理構成
-
斜め方向の管理用配列
【問題文】では「盤面の利き筋の方向別に配列 col(列方向)、upwd(斜め上方向)及び downwd(斜め下方向)を用意した」とあり、各マスにクイーンを置くと 3 本の配列要素を NOT_FREE にします。 -
upwd の添字規則は与えられている
同じ文中で「upwd[i+k-1]」と添字式を明示しています。斜め上方向は行番号 i と列番号 k の“和”が一定であるため、i+k が基準になるのは自然です。 -
downwd の本質は “行−列” が一定
斜め下方向は行番号と列番号の“差” i-k が同じマスを結びます。 -
負値を回避するためのオフセット
差 i-k は- 最小で 1-N(一番左上のマス)
- 最大で N-1(一番右下のマス)
と負の値を取り得ます。配列添字は 1 以上の整数でそろえたいので、定数を足してシフトします。
-
0 ではなく N を加える理由
- 値域 1-N … N-1 に N を加えると 1 … 2N-1 に変換できます。
- この範囲は【問題文】の「upwd と downwd の大きさはともにアである」から、配列長が 2N-1 であることと整合します。
-
結論
以上より「downwd[イ]」の添字はi - k + Nとなり、模範解答の「イ:i-k+N」と一致します。
誤りやすいポイント
- upwd と同じく +1 オフセットを付けて i-k+N-1 とずらしてしまう。
- i-k が負になるケースを考えずにそのまま配列添字に用いて実行時エラーを起こす。
- 差ではなく和を用いて upwd と downwd を混同する。
FAQ
Q: なぜ +N でなく +N-1 ではだめなのですか?
A: 最小値 1-N に +N-1 を足すと 0 になり、配列添字は 1 から始める前提と矛盾します。+N を足せば下限が 1 になります。
A: 最小値 1-N に +N-1 を足すと 0 になり、配列添字は 1 から始める前提と矛盾します。+N を足せば下限が 1 になります。
Q: 配列長が 2N-1 であることを簡単に確認する方法は?
A: 差の取りうる値の個数は N(負側)+1(0)+N-1(正側)で計 2N-1 です。全てに 1 を足して 1 から 2N-1 の添字で管理できます。
A: 差の取りうる値の個数は N(負側)+1(0)+N-1(正側)で計 2N-1 です。全てに 1 を足して 1 から 2N-1 の添字で管理できます。
Q: 盤面を 0 始まりの添字で実装したいときは?
A: その場合は upwd を i+k、downwd を i-k+(N-1) とし、配列長を 2N-1 のまま 0〜2N-2 の範囲で扱えば整合します。
A: その場合は upwd を i+k、downwd を i-k+(N-1) とし、配列長を 2N-1 のまま 0〜2N-2 の範囲で扱えば整合します。
関連キーワード: バックトラッキング、再帰呼び出し、配列インデックス、オフセット計算、二次元座標
設問3:〔Nクイーン問題の解法のプログラム〕について、(1)〜(3)に答えよ。
(1)図7中のウ〜キに入れる適切な字句を答えよ。
模範解答
ウ:k
エ:l
オ:N
カ:posi ← k
キ:i+1
解説
解答の論理構成
-
ループ変数 ウ
図7には
if ( colk と upwd[i+k-1] と downwd[ ... ] … )
とあり、配列添字に使われているのは 「k」 だけです。従ってループで増減させる変数も 「k」 とするのが自然です。 -
ループ開始値 エ
解法の手順には
「1行目において、1列目にクイーンを配置する。次に…2行目の列を1列目から順に探索し…」
とあります。列探索は必ず 「1列目から」 始めると明記されているため、開始値は 「1」 です。 -
ループ終了値 オ
N×N 盤の全列を調べきるまで探索を続けるため、終了値は最大列番号である 「N」 です。 -
クイーンを配置した列を記録する カ
配列 pos の定義は
「行番号を添字とし、その行に配置したクイーンの位置(列番号)を値とする」
ですから、行 i にクイーンを置いた列 k を格納する代入文は
posi ← k となります。 -
次の行を再帰探索する キ
1 行に 1 個ずつクイーンを置く方針なので、行 i の次は 「i+1 行目」 を探索します。したがって再帰呼び出しの実引数は 「i+1」 です。
誤りやすいポイント
- 変数名の取り違え
k と i を混同し、列探索を i で回してしまうと行番号と列番号が逆転します。 - ループ開始値の誤記
「列1から探索」という文面を読み落として 0 から開始すると、配列外参照や列番号ずれを起こします。 - pos の更新忘れ
クイーンを配置した時点で posi を更新しないと、バックトラック後に正しい出力が得られません。
FAQ
Q: ループ変数を c など他の文字にしてはいけませんか?
A: ソースコード自体は任意名で動きますが、問題文が colk など 「k」 を前提に説明しているため、解答欄には一致させる必要があります。
A: ソースコード自体は任意名で動きますが、問題文が colk など 「k」 を前提に説明しているため、解答欄には一致させる必要があります。
Q: i+1 でなく ++i でも良いですか?
A: 再帰呼び出しの実引数としては「次の行番号」の値そのものが必要です。++i は呼び出し後の i を変更してしまう恐れがあるため、問題の意図に合わせて i+1 を使います。
A: 再帰呼び出しの実引数としては「次の行番号」の値そのものが必要です。++i は呼び出し後の i を変更してしまう恐れがあるため、問題の意図に合わせて i+1 を使います。
Q: 0 オリジンで実装するときの添字計算はどう直せばよいですか?
A: 行・列を 0〜N-1 とする場合、upwd の添字は i+k、downwd は i-k+N-1 など、すべての式を 1 だけ減算または加算して整合を取ります。
A: 行・列を 0〜N-1 とする場合、upwd の添字は i+k、downwd は i-k+N-1 など、すべての式を 1 だけ減算または加算して整合を取ります。
関連キーワード: バックトラック、再帰呼出し、探索アルゴリズム、配列インデックス、Nクイーン
設問3:〔Nクイーン問題の解法のプログラム〕について、(1)〜(3)に答えよ。
(2)図8中のクに入れる適切な字句を答えよ。
模範解答
ク:l
解説
解答の論理構成
- 【問題文】には最初の作業として
「・1行目において、1列目にクイーンを配置する。」
と明記されています。したがって探索は“行1”から始まります。 - 図7の関数 search(i) は「i行目以降についてクイーンの配置の仕方を探索する」手続きです。
つまり、行1から探索を開始したい場合は i = 1 を与えます。 - 図8のメインプログラムでは
if ( search( ク ) と SUCCESS が等しい ) then
と書かれており、ここに渡す値が“開始行”になります。 - 以上より ク には 1 を入れ、search(1) として探索を開始するのが論理的帰結です。
- よって解答は
「ク:1」
誤りやすいポイント
- 0 行目や N 行目から探索を始めると考えてしまう
→ 配列添字を 0 起点で実装する言語に慣れていると誤解しやすいですが、本問題では【問題文】が「1行目」からの記述です。 - pos 配列の初期値が 0 なので「行番号も 0 から」と連想してしまう
→ pos の 0 は「未配置」を表す特別値であって行番号ではありません。 - search の呼出しで変数を渡すと誤認する
→ 図8には引数として変数名は示されておらず、固定値を渡す想定です。
FAQ
Q: 行番号を 0 起点に書き換えても動きますか?
A: アルゴリズム自体は動きますが、配列添字計算(特に upwd や downwd)をすべて修正する必要があります。本試験では【問題文】が 1 起点なので、そのまま従うのが安全です。
A: アルゴリズム自体は動きますが、配列添字計算(特に upwd や downwd)をすべて修正する必要があります。本試験では【問題文】が 1 起点なので、そのまま従うのが安全です。
Q: 途中で複数の解を列挙したい場合はどう変更しますか?
A: search が解を見つけた時点で戻らずに盤面を記録し、クイーンを取り除いて探索を続行するようにします。戻り値で探索終了を制御しない実装へ変えるのが一般的です。
A: search が解を見つけた時点で戻らずに盤面を記録し、クイーンを取り除いて探索を続行するようにします。戻り値で探索終了を制御しない実装へ変えるのが一般的です。
Q: search(1) をループで呼び直せば全部の解が得られますか?
A: いいえ。search から戻る時点で盤面は初期状態へ巻き戻っているため、再度 search(1) を呼んでも同じ解しか生成されません。呼出し側ではなく search 内部で分岐を制御する必要があります。
A: いいえ。search から戻る時点で盤面は初期状態へ巻き戻っているため、再度 search(1) を呼んでも同じ解しか生成されません。呼出し側ではなく search 内部で分岐を制御する必要があります。
関連キーワード: バックトラック、再帰呼出し、配列インデックス、盤面探索、斜め判定
設問3:〔Nクイーン問題の解法のプログラム〕について、(1)〜(3)に答えよ。
(3)4×4マスの場合、このプログラムによる解を図9に示す。この結果が得られるまでに、図7中の①の部分は何回実行されるか答えよ。


模範解答
4
解説
解答の論理構成
-
初期状態
メインプログラム(図8)の
if ( search( ク ) …
で search(1) が呼ばれ、全配列は FREE、pos は全て 0 です。 -
探索木の展開順序
関数 search は図7の
for ( ウ を エ から オ まで1ずつ増やす )
で列 k を 1 → 2 → 3 → 4 の順に調べます。 -
① が実行される条件
図7で示された ① は
// クイーンを取り除く
posi ← 0
colk ← FREE
upwd[i+k-1] ← FREE
downwd[ イ ] ← FREE
posi ← 0
colk ← FREE
upwd[i+k-1] ← FREE
downwd[ イ ] ← FREE
という “バックトラック” 部分です。
すなわち「一度置いたクイーンの後続行で FAILURE が返されたとき」に ① が1回実行されます。
すなわち「一度置いたクイーンの後続行で FAILURE が返されたとき」に ① が1回実行されます。
-
4×4 マスにおける実行過程
(各行の “→” は実際にクイーンを置いた列、×は衝突で置けなかった列を表す)① が実行されたタイミングは次の4回です。- (2 行 3 列) を取り除く
- (3 行 2 列) を取り除く
- (2 行 4 列) を取り除く
- (1 行 1 列) を取り除く
-
結論
以上より、図9 の解に到達するまでに
「図7中の①の部分」は 4 回 実行されます。
誤りやすいポイント
- ① は “列を1つ進めるたび” ではなく、“再帰呼び出しが FAILURE を返したときだけ” 実行されます。
- 同じ行で複数回クイーンを置き換えても、① が走るのは “取り除くときだけ” であり、“置き直す前の衝突判定” では走りません。
- 盤上で置ける列が 0 本になった行では その行では①が発生しない 点を忘れがちです(①は「置いた後」にしか現れないため)。
FAQ
Q: ① が 5 回以上になると考えてしまいました。どこで数え間違えやすいですか?
A: 行 3 で “1つも置けずにただ戻る” 場面があります。このときはクイーンを置いていないので①は呼ばれません。ここをカウントしてしまうと 1 回多くなります。
A: 行 3 で “1つも置けずにただ戻る” 場面があります。このときはクイーンを置いていないので①は呼ばれません。ここをカウントしてしまうと 1 回多くなります。
Q: 解が見つかったあとにも①は動きますか?
A: 図7の return SUCCESS が発生したら親関数もすぐ return SUCCESS へ伝播するため、以降の①は実行されません。
A: 図7の return SUCCESS が発生したら親関数もすぐ return SUCCESS へ伝播するため、以降の①は実行されません。
Q: 別の列順(例:右から左)で探索すると①の回数も変わりますか?
A: はい。バックトラックの回数は探索順に依存します。今回の答え「4」は “列を 1 から 4 へ順に試す” という本プログラム固有の順序での結果です。
A: はい。バックトラックの回数は探索順に依存します。今回の答え「4」は “列を 1 から 4 へ順に試す” という本プログラム固有の順序での結果です。
関連キーワード: バックトラック、再帰探索、状態空間木、コンビナトリック探索、配列フラグ


