応用情報技術者 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:
表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:
図3中のウ〜キに入れる適切な字句を答えよ。
模範解答
ウ:iがm×n
エ:i+1
オ:v+dv[j]
カ:h+dh[j]
キ:board[v][h] ← 0
解説
解答の論理構成
-
【問題文】では “記録した移動順序が M×N に等しい場合、その経路を解の一つとして出力する” とあります。
図3の6行目はまさにその判定を行う位置なので、条件部 [ウ] には “iがm×n” が入ります。
(プログラム中では定数 M, N を変数 m, n に格納しているため) -
続く10〜12行目は “現在のマスを起点とした図1の移動先①〜⑧のそれぞれについて再帰関数 search を呼び出す。… 移動順序には、現在の移動順序に 1 を加えたものを指定する” という処理に対応します。
よって
・次の移動順序 → i+1 = [エ]
・次の行番号 → v+dv[j] = [オ]
・次の列番号 → h+dh[j] = [カ] -
【問題文】“現在のマスの移動順序を取り消して、マスを通過していない状態に戻す” に当たるのが図3の14行目。
要素を初期値 0 に戻せば未通過状態になるので [キ] は board[v][h] ← 0 です。
以上より
[ウ] iがm×n
[エ] i+1
[オ] v+dv[j]
[カ] h+dh[j]
[キ] board[v][h] ← 0
[ウ] iがm×n
[エ] i+1
[オ] v+dv[j]
[カ] h+dh[j]
[キ] board[v][h] ← 0
誤りやすいポイント
- “i==m*n” と記述してしまう
本問の擬似言語は “~が …” を等価比較に使う表記であり、== は不要です。 - 配列添字を逆に書く
行方向を dv[]、列方向を dh[] と定義しているので、v+dh[j]/h+dv[j] としないよう注意。 - 取り消し処理を忘れる
深さ優先探索ではバックトラッキングが必須。“board[v][h] ← 0” を省くと通過履歴が残り誤判定になります。
FAQ
Q: “iがm×n” と “i = m×n” は同じですか?
A: 本問の擬似言語では “~が …” が等価比較を表します。“=” は代入と解釈されるため誤りです。
A: 本問の擬似言語では “~が …” が等価比較を表します。“=” は代入と解釈されるため誤りです。
Q: dv[], dh[] を使わず直接座標を列挙しても良いですか?
A: 計算結果は同じですが、配列化しておくと盤面サイズや探索順序の変更が容易になり、コードも簡潔になります。
A: 計算結果は同じですが、配列化しておくと盤面サイズや探索順序の変更が容易になり、コードも簡潔になります。
Q: 取り消し処理は探索の最後に1回まとめて行えませんか?
A: できません。各再帰呼び出しから戻るたびにそのマスを未通過に戻さないと、他の経路探索で通路が閉ざされます。
A: できません。各再帰呼び出しから戻るたびにそのマスを未通過に戻さないと、他の経路探索で通路が閉ざされます。
関連キーワード: 深さ優先探索, 再帰呼び出し, バックトラッキング, 配列添字, 条件判定
設問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配列, 深さ優先探索, 再帰呼び出し, バックトラッキング


