応用情報技術者 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:
問題文を見るN×Nマスの場合、本文及び図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:
問題文を見るN×Nマスの場合、本文及び図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)に答えよ。
問題文を見る模範解答
ウ:k
エ:1
オ:N
カ:pos[i] ← k
キ:i+1
解説
解答の導き方
図7の for 文と、その中の処理を一つずつ本文の記述と突き合わせて決めます。
-
ループ変数(ウ)
for 文の直後の条件が「col[k] と upwd[i+k−1] と downwd[[イ]] が全て FREE と等しい」となっており、列を表す添字は k です。関数 search は i 行目について列を順に試すので、ループ変数は列番号の k です。
→ [ウ] = k -
開始値(エ)と終了値(オ)
本文は解法を「1 行目において、1 列目にクイーンを配置する。次に 2 行目において、1 列目から順に、…クイーンが配置できる列を探索する」と説明しています。各行の探索は 1 列目から始まり、本文の「N×N マスの場合、配列 col の大きさは N」のとおり列番号は 1〜N です。
→ [エ] = 1、[オ] = N
本文の「取り除いたクイーンの次の列以降で…探索する」という動作は、開始値を変えなくても実現できます。後続の行で FAILURE が返ると①でクイーンを取り除き、同じ for 文が k を 1 増やして次の列を試すからです。 -
クイーンを配置する処理(カ)
本文に「配列 pos は、行番号を添字とし、その行に配置したクイーンの位置(列番号)を値とする」とあり、①では「pos[i] ← 0」で取り除いています。配置するときはその逆で、i 行目に列番号 k を記録します。
→ [カ] = pos[i] ← k -
再帰呼出しの引数(キ)
search は「i 行目以降についてクイーンの配置の仕方を探索する」関数です。i 行目に置けたら、次は i+1 行目以降を探索します。
→ [キ] = i+1
誤りやすいポイント
- エを「前回試した列の次」を表す変数にしてしまう。再試行は for 文が k を進めることで行われるので、開始値は常に 1 です。
- ループ変数を行番号の i と取り違える。行は search の引数 i、列は for 文の k です。
- カで「col[k] ← NOT_FREE」を答える。この処理は図7で既にカの次の行に書かれており、カは pos の記録です。
FAQ
Q: 後戻りした行で、なぜ 1 列目から試し直さずに済むのですか?
A: 後戻りは「search(i+1) が FAILURE を返した」ときに、i 行目の for 文の途中で起こります。①でクイーンを取り除いた後、for 文はそのまま k+1 列目に進むので、既に試した列を繰り返すことはありません。
A: 後戻りは「search(i+1) が FAILURE を返した」ときに、i 行目の for 文の途中で起こります。①でクイーンを取り除いた後、for 文はそのまま k+1 列目に進むので、既に試した列を繰り返すことはありません。
Q: pos を記録するのは何のためですか?
A: 解が見つかったとき、各行のクイーンの列番号を印字するためです。図8 では search が SUCCESS を返したときに「解となるクイーンの配置を印字する」としています。
A: 解が見つかったとき、各行のクイーンの列番号を印字するためです。図8 では search が SUCCESS を返したときに「解となるクイーンの配置を印字する」としています。
関連キーワード: バックトラック、再帰、深さ優先探索、Nクイーン問題
設問3:〔Nクイーン問題の解法のプログラム〕について、(1)〜(3)に答えよ。
問題文を見る(2)図8中のクに入れる適切な字句を答えよ。
模範解答
ク:1
解説
解答の導き方
図8のメインプログラムは「if ( search( [ク] ) と SUCCESS が等しい ) then」で探索を始めます。何行目から探索を始めるかを決めます。
-
関数 search の引数の意味
本文に「i 行目以降についてクイーンの配置の仕方を探索する再帰関数 search」とあります。引数は探索を始める行番号です。 -
探索の開始位置
本文の解法は「1 行目において、1 列目にクイーンを配置する」から始まります。盤面全体(1 行目以降)の配置を探すので、最初の呼出しでは 1 行目を指定します。 -
結論
→ [ク] = 1
search(1) の中で 1 行目のクイーンを置き、search(2)、search(3)、…と再帰的に次の行へ進みます。
誤りやすいポイント
- 0 を答える。本文の行番号・列番号は 1 から始まり、図3〜6 の配列の添字も 1 からです。
- N を答える。search は「i 行目以降」を探索するので、N を渡すと最後の行しか探索しません。
FAQ
Q: search(1) が FAILURE を返すのはどんなときですか?
A: 1 行目のどの列にクイーンを置いても、残りの行に配置できない場合です。このとき図8 では解がないことになります。
A: 1 行目のどの列にクイーンを置いても、残りの行に配置できない場合です。このとき図8 では解がないことになります。
関連キーワード: 再帰呼出し、バックトラック、Nクイーン問題
設問3:〔Nクイーン問題の解法のプログラム〕について、(1)〜(3)に答えよ。
問題文を見る(3)4×4マスの場合、このプログラムによる解を図9に示す。この結果が得られるまでに、図7中の①の部分は何回実行されるか答えよ。


模範解答
4
解説
解答の論理構成
-
① が実行される条件
図7で示された ① は
// クイーンを取り除く
pos[i] ← 0
col[k] ← FREE
upwd[i+k-1] ← FREE
downwd[ イ ] ← FREE
pos[i] ← 0
col[k] ← 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へ順に試す” という本プログラム固有の順序での結果です。
関連キーワード: バックトラック、再帰探索、状態空間木、コンビナトリック探索、配列フラグ





