応用情報技術者 2018年 春期 午後 問03
ナイトの巡歴問題に関する次の記述を読んで、設問1〜3に答えよ。
ナイトの巡歴問題とは、M行N列(以下、M×Nマスという)の盤面上でチェスの駒の一種であるナイトを移動させ、全てのマスを1回ずつ通過する経路を発見する問題である。
ナイト(K)が1回で移動できるマス(以下、移動先という)の位置を図1に示す。また、4×3マスの場合のナイトの巡歴問題の解の一つを図2に示す。図2に示すナイトの移動する順序を表す数を、移動順序という。
なお、行番号は上から下、列番号は左から右に増加する。

M×Nマスのナイトの巡歴問題について、行1列1のマスを起点とする全ての経路を求める処理の概要を示す。この処理は、再帰による深さ優先探索として実装されている。
〔ナイトの巡歴問題の処理の概要〕
(1) 移動順序1、行1、列1で、再帰関数searchを呼び出す。
再帰関数search( 移動順序、行、列 )
(i) 行と列で指定されるマス(以下、現在のマスという)が盤面の範囲外、又は既に通過したマスであった場合、何もせずに再帰関数searchの呼出し元へ戻る。
(ii) (i) 以外の場合、現在のマスに、移動順序を記録する。
(ii-1) 記録した移動順序がM×Nに等しい場合、その経路を解の一つとして出力する。
(ii-2) (ii-1) 以外の場合、現在のマスを起点とした図1の移動先①〜⑧のそれぞれについて再帰関数searchを呼び出す。引数の行と列には、移動先を指定する。移動順序には、現在の移動順序に1を加えたものを指定する。
(ii-3) 現在のマスの移動順序を取り消して、マスを通過していない状態に戻す。
(ii-4) 再帰関数searchの呼出し元へ戻る。
(2) 終了する。
この処理の概要をプログラムに実装するために、M×Nマスの盤面、ナイトの移動先をそれぞれ次のデータ構造で表現することにした。
・M×Nマスの盤面を2次元配列board[M][N] で表現する。添字は1から始まる。各要素の初期値は0とし、ナイトが通過した場合に、移動順序を要素に格納する。
・ナイトの各移動先について、行方向、列方向、それぞれの移動量をdv[]、dh[] の配列で表現する。添字は1から始まる。dv[]、dh[] はそれぞれ、八つの要素をもち、図1の移動先①〜⑧に対応する行方向、列方向の移動量を設定する。
dv[]、dh[] に設定する値を表1に示す。①の場合、行方向は上に2マス、列方向は右に1マス移動するので、dv[1] は -2、dh[1] は1となる。
〔ナイトの巡歴問題の解法のプログラム〕
M×Nマスのナイトの巡歴問題について、解法のプログラムを考える。
解法のプログラムで使用する定数、変数及び関数を表2に示す。

再帰関数searchのプログラムを図3に、解答を印字する関数printBoardのプログラムを図4に、メインプログラムを図5に示す。
なお、左側の番号はプログラムの行番号を示す。


〔盤面の表現の変更〕
ナイトの移動先が盤面の範囲外となった場合の判定処理を簡略化するために、図6のように盤面をナイトが移動できるマスが全て含まれる範囲まで拡大して表現する。
この変更に合わせて①関数printBoardの変更、②メインプログラムの変更、③再帰関数searchの一部の行の削除を同時に行うことによって、プログラムを短縮することができる。

設問1:
問題文を見る模範解答
ア:2
イ:-2
解説
解答の論理構成
-
【問題文】ではナイトの8方向を “図1の移動先①〜⑧” と定義し、
「①の場合、行方向は上に2マス、列方向は右に1マス移動するので、dv[1] は -2、dh[1] は1となる。」
と示しています。ここから
・上方向は負値、下方向は正値
・右方向は正値、左方向は負値
で表すという符号規則が確定します。 -
④の位置を図1で確認すると、中央 (3行3列) から “下へ2、右へ1” です。
行方向 +2 ⇒ dv[4] = 2
列方向 +1 ⇒ dh[4] = 1 (すでに表1に掲載済み)
したがって ア は2となります。 -
⑥の位置は “下へ1、左へ2” です。
行方向 +1 ⇒ dv[6] = 1 (既定値)
列方向 -2 ⇒ dh[6] = -2
よって イ は -2となります。 -
以上より
ア:2
イ:-2
となり、【模範解答】と一致します。
誤りやすいポイント
- “上方向が負、左方向が負” という符号規則を逆に覚えるミス
- 図1の番号と実際の座標変位を取り違え、⑤と⑥などを入れ替えてしまうミス
- 行方向・列方向を0始まり配列と混同し、±1をずらして計算してしまうミス
FAQ
Q: 盤面の添字は1始まりですが、dv[]/dh[] の値決定に影響しますか?
A: いいえ。行・列の差分だけを見ているため、基準が1始まりか0始まりかは無関係です。
A: いいえ。行・列の差分だけを見ているため、基準が1始まりか0始まりかは無関係です。
Q: dv[] とdh[] の符号を一度で覚えるコツは?
A: 図1の “①(-2, +1)” を基準にすると、上が負・右が正というルールを自然に覚えられます。
A: 図1の “①(-2, +1)” を基準にすると、上が負・右が正というルールを自然に覚えられます。
Q: 盤面を拡大する“番兵法”を入れてもdv[]/dh[] の値は変わりますか?
A: 変わりません。番兵法は配列外判定を簡略化するだけで、ナイトの相対移動量そのものは不変です。
A: 変わりません。番兵法は配列外判定を簡略化するだけで、ナイトの相対移動量そのものは不変です。
関連キーワード: バックトラッキング, 深さ優先探索, 再帰関数, 座標変換
設問2:
問題文を見る模範解答
ウ:iがm×n
エ:i+1
オ:v+dv[j]
カ:h+dh[j]
キ:board[v][h] ← 0
解説
解答の導き方
図3の再帰関数searchは問題文の処理の概要の再帰部分を擬似コードで表したものです。表2の定義から「iはナイトの移動順序を表す変数」「vは調べるマスの行番号を表す変数」「hは調べるマスの列番号を表す変数」であることが分かります。以下、図3の行番号に沿って空欄を埋める根拠を順を追って示します。
-
現在のマスへの記録(図3の行5)
図3の行5で board[v][h] ← i により現在のマスに移動順序を記録します。問題文には「現在のマスに、移動順序を記録する。」とあるので、この直後の条件判定(図3の行6)は「記録した移動順序が全マスに達したか」を調べる場所です。問題文の(ii-1)には「記録した移動順序がM×Nに等しい場合、その経路を解の一つとして出力する。」とあるため、図3の行6の条件は となります。ここで main(図5の行28〜29)の「m ← M」「n ← N」により が盤面の行数・列数を表すため、総マス数は です。 -
再帰呼び出しの引数(図3の行10〜11)
図3の行10はfor( jを1から8まで ) で各移動先①〜⑧を順に調べます。問題文に「移動順序には、現在の移動順序に1を加えたものを指定する。引数の行と列には、移動先を指定する。」とあるので、再帰呼び出しの第一引数は です。移動先の行・列はdv[]、dh[] に対応するオフセットを現在の行・列に加えたものであると表記してある(「dv[]、dh[] はそれぞれ、八つの要素をもち、図1の移動先①〜⑧に対応する」)ため、第二引数は 、第三引数は となります。 -
バックトラック(図3の行14)
再帰探索が戻ってきた後は「現在のマスの移動順序を取り消して、マスを通過していない状態に戻す。」((ii-3))必要があります。図3の行5で board[v][h] に値を入れているので取り消しは board[v][h] ← 0 です。
以上を踏まえると各空欄の値は次のとおりになります。
- ウ:
- エ:
- オ:
- カ:
- キ:board[v][h] ← 0
誤りやすいポイント
- 「=」の意味の取り違え:図3の行6は条件式なので等価比較の表現にして下さい。答案としては 、あるいは「iがm×n」など等価を示す記述で正答となります。擬似コードの記法(言語により '=' が代入になる場合がある点)と混同しないよう注意してください。
- iを増やし忘れる:再帰呼び出しで移動順序を増やさずにsearch(i, ...) のようにしてしまうと探索が正しく進みません。必ず を渡します。
- バックトラック忘れ:board[v][h] を0に戻さないと、そのマスが既に通過済みと見なされて他の経路が探索できなくなります。必須の操作です。
- 添字の基点を誤る:dv[]、dh[] は問題文で添字を1から始めると明示されています。forループがjを1から8まで回すことと整合させて下さい。
- dv/dhの役割混同:dv[] は行方向、dh[] は列方向のオフセットです。行と列を取り違えないようにします。
FAQ
Q: 図3の行6に「i = m×n」と書くのは代入ですか、比較ですか?
A: 図3の行6はifの条件判定なので比較(等価)です。試験の解答では 、 のような表現や「iがm×n」といった自然言語表現のいずれも等価な意味で受け入れられます(擬似コードの記法は問題ごとに異なるので、文脈で条件を満たす表現にすることが重要です)。
A: 図3の行6はifの条件判定なので比較(等価)です。試験の解答では 、 のような表現や「iがm×n」といった自然言語表現のいずれも等価な意味で受け入れられます(擬似コードの記法は問題ごとに異なるので、文脈で条件を満たす表現にすることが重要です)。
Q: 再帰呼び出しでdv[] とdh[] を使うのはなぜですか?
A: 問題文に「dv[]、dh[] はそれぞれ、八つの要素をもち、図1の移動先①〜⑧に対応する」とあるため、各移動先の行・列の変位はdv[j], dh[j] に格納されています。現在の位置v,hにそれらを足すことで各移動先の座標 , が得られます。
A: 問題文に「dv[]、dh[] はそれぞれ、八つの要素をもち、図1の移動先①〜⑧に対応する」とあるため、各移動先の行・列の変位はdv[j], dh[j] に格納されています。現在の位置v,hにそれらを足すことで各移動先の座標 , が得られます。
Q: board[v][h] ← 0 は省略できますか?
A: 省略するとバックトラックできず、他の経路を探索できなくなります。必ず元に戻す操作が必要です(図3の行14)。
A: 省略するとバックトラックできず、他の経路を探索できなくなります。必ず元に戻す操作が必要です(図3の行14)。
関連キーワード: 深さ優先探索、バックトラッキング、再帰、二次元配列、ナイトの巡歴問題
設問3:〔盤面の表現の変更〕について、(1)〜(3)に答えよ。
問題文を見る(1)本文中の下線①について、関数printBoardのプログラムで変更が必要な行の行番号と、変更後のプログラムを、2か所答えよ。
模範解答
①:
行番号:20
変更後のプログラム:for(vを3からm+2まで1ずつ増やす)
②:
行番号:21
変更後のプログラム:for(hを3からn+2まで1ずつ増やす)
解説
解答の論理構成
-
盤面の拡大理由
- 問題文では「ナイトの移動先が盤面の範囲外となった場合の判定処理を簡略化するために、図6のように盤面を…拡大して表現する」とあります。
- 図6の“変更後の盤面”を見ると、実際に移動可能な範囲の外側に幅 2 マス分の“番兵(値1)”を配置しています。
-
元のprintBoardの挙動
-
行番号 20 と 21 はそれぞれfor( v を 1 から m まで 1 ずつ増やす ) for( h を 1 から n まで 1 ずつ増やす )
-
これは拡大前(番兵なし)を前提に「実盤面だけ」を表示するループです。
-
-
拡大後に必要なオフセット
- 図6より、実盤面の左端は列 3、上端は行 3 にシフトしています。
- 右端・下端も同様に 2 マス分だけ後ろへ伸びているので、最終行列はm+2, n+2になります。
-
従って修正箇所
-
行番号 20 をfor( v を 3 から m+2 まで 1 ずつ増やす )
-
行番号 21 をfor( h を 3 から n+2 まで 1 ずつ増やす )
と書き換えることで、番兵を除いた“真の盤面”のみを印字できます。 -
誤りやすいポイント
- 「幅1マス」ではなく 2マス の番兵が付くことを見落としやすい。
- searchの境界判定を削除した結果、printBoardも変更が必要と気づかずそのままにしてしまう。
- mとnを更新して盤面全体を8×7にする、と誤解してループ上限をm、nで置き換えない。
FAQ
Q: ループ下限を2でなく3にする決め手は何ですか?
A: 図6では上端・左端に 2 行・2列の番兵があり、実盤面はその直後の行 3・列 3 から始まります。
A: 図6では上端・左端に 2 行・2列の番兵があり、実盤面はその直後の行 3・列 3 から始まります。
Q: m+2とn+2の +2は常に固定ですか?
A: はい。番兵はナイトの最大移動量(縦横いずれも2マス)に合わせて付けているため、盤面サイズに依存せず 2 マスで固定です。
A: はい。番兵はナイトの最大移動量(縦横いずれも2マス)に合わせて付けているため、盤面サイズに依存せず 2 マスで固定です。
Q: printBoardだけ直せばよいのですか?
A: 問題文には「①関数printBoardの変更、②メインプログラムの変更、③再帰関数searchの一部の行の削除」とあり、他の箇所も合わせて修正が必要です。本設問は①のみを問うています。
A: 問題文には「①関数printBoardの変更、②メインプログラムの変更、③再帰関数searchの一部の行の削除」とあり、他の箇所も合わせて修正が必要です。本設問は①のみを問うています。
関連キーワード: 再帰, 深さ優先探索, バックトラック, 配列オフセット, ボーダーパディング
設問3:〔盤面の表現の変更〕について、(1)〜(3)に答えよ。
問題文を見る(2)本文中の下線②について、メインプログラムで変更が必要な行の行番号と、変更後のプログラムを、1か所答えよ。
模範解答
行番号:32
変更後のプログラム:search(1,3,3)
解説
解答の論理構成
-
変更前のメインプログラムは
「32: search( 1, 1, 1 )」
と記載され、searchの第2・第3引数に「行1、列1」を渡していました(【問題文】図5)。これは“行1列1のマスを起点とする”という処理仕様(【問題文】「(1) 移動順序1、行1、列1で、再帰関数searchを呼び出す。」)に対応しています。 -
〔盤面の表現の変更〕では
「盤面をナイトが移動できるマスが全て含まれる範囲まで拡大して表現する。」
と明記されており、盤面の周囲に番人(境界用の1)が2マス分置かれる構造に変わります。この“2マス分”の根拠は、ナイトの最大移動幅が縦横方向いずれも2であるためです(【問題文】表1のdv[], dh[] 参照)。 -
拡大後の配列board[][] では、
元々の「行1列1」は “外側に2マス分” シフトし、
内部表現上の座標は「行3列3」になります。 -
したがって、起点を正しく指定するには
行番号・列番号ともに +2した値をsearchに渡す必要があります。 -
よって、変更箇所(行番号)と変更後の呼び出しは
【模範解答】
行番号:32
変更後のプログラム:search(1,3,3)
となります。第1引数「1」は移動順序を示すため変更しません。
誤りやすいポイント
- 「+2のシフト」に気付きながら、第1引数まで3に変更してしまう。移動順序は常に1から開始するため不正解になります。
- 盤面を広げたあとも境界判定コードを削除し忘れ、テスト時に動作がおかしくなる。
- シフト量を1だけと誤認し「search(1,2,2)」と書いてしまう。ナイトは2マス跳ぶことを思い出しましょう。
FAQ
Q: なぜ“番人”を2マス分置くだけで境界判定が不要になるのですか?
A: ナイトは最大でも縦2・横2しか進めません(【問題文】表1)。外周に2マス分の番人を置けば、元の盤面内から1回移動しただけで番人領域のさらに外へ飛び出すことがなくなるため、vとhの範囲チェックを削除できます。
A: ナイトは最大でも縦2・横2しか進めません(【問題文】表1)。外周に2マス分の番人を置けば、元の盤面内から1回移動しただけで番人領域のさらに外へ飛び出すことがなくなるため、vとhの範囲チェックを削除できます。
Q: 拡大後の盤面サイズは固定ですか?
A: 元のサイズをm×nとすると、行方向はm+4、列方向もn+4へ拡張されます(上下左右に2行・2列ずつ番人を付与)。このサイズは実装時に計算で求めるのが一般的です。
A: 元のサイズをm×nとすると、行方向はm+4、列方向もn+4へ拡張されます(上下左右に2行・2列ずつ番人を付与)。このサイズは実装時に計算で求めるのが一般的です。
Q: printBoard() 内のループ範囲を変えなくても良いのはなぜ?
A: 〔盤面の表現の変更〕で「①関数printBoardの変更」も指示されています。印字対象を番人を除く領域(行3〜m+2, 列3〜n+2)に限定するよう修正するため、メイン側の呼び出し位置だけ合わせれば正しい解が出力されます。
A: 〔盤面の表現の変更〕で「①関数printBoardの変更」も指示されています。印字対象を番人を除く領域(行3〜m+2, 列3〜n+2)に限定するよう修正するため、メイン側の呼び出し位置だけ合わせれば正しい解が出力されます。
関連キーワード: 深さ優先探索, 再帰呼び出し, 番人法, 座標シフト, チェスナイト問題
設問3:〔盤面の表現の変更〕について、(1)〜(3)に答えよ。
問題文を見る(3)本文中の下線③について、再帰関数searchのプログラムで削除することが必要な行の行番号を全て答えよ。
模範解答
2, 3, 16, 17
解説
解答の論理構成
- 変更前の境界判定
- 再帰関数searchの行番号 2 と 3 は
【問題文】図3
if( v が 1 以上, かつ, m 以下 )
if( h が 1 以上, かつ, n 以下 )
という二重の境界チェックを行っています。
- 再帰関数searchの行番号 2 と 3 は
- 盤面を外枠付きに拡大
- 【問題文】図6の変更後の盤面では、有効域外をすべて 1 に初期化し、
ナイトが実際に移動可能なマスだけを 0 にしています。 - この方式を取ると、配列外アクセスは発生せず、境界判定は
行 4 のif( board[v][h] が0 ) のみで十分になります。
- 【問題文】図6の変更後の盤面では、有効域外をすべて 1 に初期化し、
- 削除が必要な行
- 行 2, 3 を消すと対応する endif が不要になるため、
行 16, 17 も同時に削除する必要があります。 - これにより階層が崩れず、処理内容も所定どおり簡略化されます。
- 行 2, 3 を消すと対応する endif が不要になるため、
- 結論
- 削除対象は “2, 3, 16, 17” です。
誤りやすいポイント
- 外枠を追加したあと 4 行目のboard[v][h] が0まで削除してしまい、
「未訪問判定」ができなくなる。 - 行 2 か 3 のどちらか一方だけを残し、ロジックエラーではなく
コンパイルエラーが発生して戸惑う。 - 行番号の振り直しを忘れ、答案欄に誤った番号を記入する。
- 盤面外を 1 で埋める理由を「通過済みマスの判定」と混同し、
sentinelの役割を取り違える。
FAQ
Q: 行 4 の判定だけで本当に境界もチェックできますか?
A: はい。外枠を 1 にしているため、盤面外に出ると
board[v][h] は必ず 1 です。0でないので
行 4 の判定により再帰は打ち切られます。
A: はい。外枠を 1 にしているため、盤面外に出ると
board[v][h] は必ず 1 です。0でないので
行 4 の判定により再帰は打ち切られます。
Q: 盤面サイズを可変にした場合でもこの方法は使えますか?
A: 外枠の幅をナイトの最大移動量(今回は2マス)以上に
取れば同じロジックで動作します。
A: 外枠の幅をナイトの最大移動量(今回は2マス)以上に
取れば同じロジックで動作します。
Q: 外枠を “0” にして境界チェックを残す案はダメですか?
A: 可ですが、設問は「判定処理を簡略化する」ことが目的なので
境界チェックを削るほうが要求に合致しています。
A: 可ですが、設問は「判定処理を簡略化する」ことが目的なので
境界チェックを削るほうが要求に合致しています。
関連キーワード: sentinel配列, 深さ優先探索, 再帰呼び出し, バックトラッキング





