令和3年度 秋期 応用情報技術者試験 午後 問題 問3 強連結な有向グラフにおける一筆書きの経路を求めるアルゴリズム

学習ガイド

有向グラフ上の全ての辺を1回ずつ通る一筆書きの経路を見つけるプログラムの問題です。辺の接続情報を表す配列の構造や、探索中に経路を記録する仕組み、探索が行き詰まった際に遡る処理のアルゴリズムが問われます。例示されたグラフに対する探索の手順を図と照らし合わせて理解し、プログラム内の変数の変化を一つずつ追いかけながら穴埋め箇所を特定します。

この記事で押さえる論点

  • 点ごとの最初の接続辺を配列edgefirst、辺ごとの次の接続辺を配列edgenextで表す方法を説明できる
  • 未探索の辺をたどる処理と、行き止まりから遡る処理の手順を説明できる
  • 探索済の経路を配列searchedに積み、遡りながら配列pathの末尾から確定させる仕組みを説明できる
  • 配列searchedを配列pathに置き換えて使用する領域を減らすときの繰返し条件を説明できる

問題本文

一筆書きに関する次の記述を読んで,設問 1〜4 に答えよ。

グラフは,有限個の点の集合と,その中の 2 点を結ぶ辺の集合から成る数理モデルである。グラフの点と点の間をつなぐ辺の列のことを経路という。本問では,任意の 2 点間で,辺をたどることで互いに行き来することができる経路が存在する(以下,強連結という)有向グラフを扱う。強連結な有向グラフの例を図 1 に示す。辺は始点と終点の組で定義する。各辺には 1 から始まる番号が付けられている。

図1 強連結な有向グラフの例
図の説明テキスト

【有向グラフ】
6つのノード(a, b, c, d, e, f)と8つの有向エッジ(1〜8)からなる。

  • 1: ノードaからノードbへ
  • 2: ノードbからノードcへ
  • 3: ノードcからノードdへ
  • 4: ノードdからノードaへ
  • 5: ノードbからノードeへ
  • 6: ノードeからノードdへ
  • 7: ノードdからノードfへ
  • 8: ノードfからノードbへ

【辺の定義と凡例】
辺1=(点a, 点b) 辺2=(点b, 点c)
辺3=(点c, 点d) 辺4=(点d, 点a)
辺5=(点b, 点e) 辺6=(点e, 点d)
辺7=(点d, 点f) 辺8=(点f, 点b)
凡例 辺=(辺の始点, 辺の終点)

〔一筆書き〕

本問では,グラフの全ての辺を 1 回だけ通り,出発点から出て出発点に戻る閉じた経路をもつグラフを,一筆書きができるグラフとする。

〔一筆書きの経路の求め方〕

一筆書きの経路を求めるためには,出発点から辺の向きに従って辺を順番にたどり,出発点に戻る経路を見つける探索を行う。たどった経路(以下,探索済の経路という)について,グラフ全体で通過していない辺(以下,未探索の辺という)がない場合は,この経路が一筆書きの経路となる。未探索の辺が残っている場合は,探索済の経路を,未探索の辺が接続する点まで遡り,その点を出発点として,同じ点に戻る経路を見つけて,遡る前までの経路に連結することを繰り返す。

各点を始点とする辺を接続辺という。グラフの各点に対して接続辺の集合が決まり,辺の番号が一番小さい接続辺を最初の接続辺という。同じ始点をもつ接続辺の集合で,辺の番号を小さいものから順番に並べたときに,辺の番号が次に大きい接続辺を次の接続辺ということにする。

図 1 のグラフの各点の接続辺の集合を表 1 に示す。図 1 において,点 b の最初の接続辺は辺 2 である。辺 2 の次の接続辺は辺 5 となる。辺 5 の次の接続辺はない。

表1 図1のグラフの各点の接続辺の集合
図の説明テキスト

表1 図1のグラフの各点の接続辺の集合

点 接続辺の集合
点a 辺1
点b 辺2, 辺5
点c 辺3
点d 辺4, 辺7
点e 辺6
点f 辺8

一筆書きの経路の探索において,一つの点に複数の接続辺がある場合には,最初の接続辺から順にたどることにする。

図1のグラフで点aを出発点とした一筆書きの経路の求め方を図2に示す。

経路を構成する辺とその順番が,これ以上変わらない場合,確定済の経路という。

図2 図1のグラフで点aを出発点とした一筆書きの経路の求め方
図の説明テキスト

図1のグラフで点aを出発点とした一筆書きの経路の求め方を示す図。4つの段階[1]〜[4]が左から順に白抜き矢印でつながっている。
各段階とも図1と同じ配置で、点a(左上)と点d(左下)だけを円で示し、ほかの点は線の折れ点として描かれている。辺の番号は[1]だけに記載(辺1: aから右上へ、辺2: 右上から右下へ、辺3: 右下からdへ、辺4: dからaへ、辺5・辺6: 右上から中央左上の折れ点を経てdへ、辺7・辺8: dから中央右下の折れ点を経て右上へ)。
[1] 辺1・辺2・辺3・辺4が細い実線(探索済)、辺5・辺6・辺7・辺8が破線(未探索)。辺4の先端(点a)に矢印。
[2] 辺4が太い実線(確定済)。辺1・辺2・辺3は細い実線(探索済)、辺5〜辺8は破線(未探索)のまま。
[3] 辺4が太い実線(確定済)。辺1・辺2・辺3と、点dから辺7→辺8→辺5→辺6とたどった経路が細い実線(探索済)で、辺6の先端(点d)に矢印。
[4] 全ての辺が太い実線(確定済)。
凡例: 細い実線: 探索済, 破線: 未探索, 太い実線: 確定済
注記1 点や辺の番号を一部省略している。
注記2 矢印は経路の向きを示す。

図2を参考にした一筆書きの経路を求める手順を次に示す。

〔一筆書きの経路を求める手順〕

点aから探索する場合は,点aの最初の接続辺である辺1から始め,辺1の終点bの最初の接続辺である辺2をたどり,同様に辺3,辺4をたどる。辺4の終点aからたどれる未探索の辺は存在しないので,これ以上探索が進められない(図2[1])。

しかし,未探索の辺5,辺6,辺7,辺8が残っているので,未探索の辺が接続する点まで遡る。

終点 a から辺 4 を遡ると,辺 4 の始点 d で未探索の辺 7 が接続している。遡った経路は途中で未探索の辺が存在しないので,これ以上,辺の順番が変わらず,辺 4 は,一筆書きの経路の一部として確定済の経路となる(図 2 [2])。

点 d から同様に辺 7 → 辺 8 → 辺 5 → 辺 6 と探索できるので,辺 3 までの経路と連結した新しい探索済の経路ができる(図 2 [3])。

辺 6 の終点 d からは,辺 6 → 辺 5 → 辺 8 → 辺 7 → 辺 3 → 辺 2 → 辺 1 と出発点の点 a まで遡り,これ以上,未探索の辺がないことが分かるので,全ての辺が確定済の経路になる(図 2 [4])。

一筆書きの経路は,次の(1)〜(4)の手順で求められる。

  1. 一筆書きの経路の出発点を決める。
  2. 出発点から,未探索の辺が存在する限り,その辺をたどり,たどった経路を探索済の経路に追加する。
  3. 探索済の経路を未探索の辺が接続する点又は一筆書きの経路の出発点まで遡る。遡った経路は,探索済の経路から確定済の経路にする。未探索の辺が接続する点がある場合は,それを新たな出発点として,(2)に戻って新たな経路を見つける。
  4. 全ての辺が確定済の経路になった時点で探索が完了して,その確定済の経路が一筆書きの経路になる。

〔一筆書きの経路を求めるプログラム〕

一筆書きの経路を求める関数 directedE のプログラムを作成した。

実装に当たって,各点を点 n(n は 1〜N)と記す。例えば,図 1 のグラフでは,点 a は点 1,点 b は点 2 と記す。

グラフの探索のために,あらかじめ,グラフの点に対する最初の接続辺の配列 edgefirst 及び接続辺に対する次の接続辺の配列 edgenext を用意しておく。edgenext において,次の接続辺がない場合は,要素に 0 を格納する。

図 1 のグラフの場合の配列 edgefirst,edgenext を図 3 に示す。

図3 図1のグラフの場合の配列 edgefirst, edgenext
図の説明テキスト

配列 edgefirst と edgenext の関係を示す図。
上部に点1〜6のラベルがあり、edgefirst 配列の各要素に対応。
edgefirst の要素:[1, 2, 3, 4, 6, 8]
edgenext の上部には辺1〜8のラベルがある。
edgenext の要素:[0, 5, 0, 7, 0, 0, 0, 0]
edgefirstの要素からedgenextの要素へ矢印が引かれている:

  • edgefirstの1(点1)の枠から辺1へ矢印
  • edgefirstの2(点2)の枠から辺2へ矢印
  • edgefirstの3(点3)の枠から辺3へ矢印
  • edgefirstの4(点4)の枠から辺4へ矢印
  • edgefirstの6(点5)の枠から辺6へ矢印
  • edgefirstの8(点6)の枠から辺8へ矢印
    edgenext内で値から次のインデックスへ矢印が引かれている:
  • edgenextの辺2の値(5)から辺5へ矢印
  • edgenextの辺4の値(7)から辺7へ矢印
    注記として以下が記載:
  • edgefirst にはグラフの点に対する最初の接続辺の番号を格納している。
  • edgenext には接続辺の次の接続辺の番号を格納している。

edgefirstによって点2の最初の接続辺が辺2であることが分かり,点2から最初にたどる接続辺は辺2となる。edgenextによって,辺2の次の接続辺が辺5であることが分かるので,点2から次にたどる接続辺は辺5となる。辺5の次の接続辺はないので,点2からたどる接続辺はこれ以上ないことが分かる。

プログラム中で使用する定数と配列を表2に,作成した関数directedEのプログラムを図4に示す。

全ての配列の添字は1から始まる。

表2 使用する定数と配列
図の説明テキスト

表2 使用する定数と配列

名称 種類 内容
N 定数 グラフの点の個数
M 定数 グラフの辺の個数
start[m] 配列 start[m] には,辺 m の始点の番号が格納されている。
end[m] 配列 end[m] には,辺 m の終点の番号が格納されている。
edgefirst[n] 配列 edgefirst[n] には,点 n の最初の接続辺の番号が格納されている。
edgenext[m] 配列 edgenext[m] には,辺 m の次の接続辺の番号が格納されている。次の接続辺がない場合は 0 が格納されている。
current[n] 配列 current[n] には,点 n を始点とする未探索の辺の中で最小の番号を格納する。点 n を始点とする未探索の辺がない場合は 0 を格納する。
searched[m] 配列 一筆書きの経路を構成する探索済の辺の番号を順番に格納する。(探索済の経路)
path[m] 配列 一筆書きの経路を構成する確定済の辺の番号を順番に格納する。(確定済の経路)
図4 関数 directedE のプログラム
図の説明テキスト

関数 directedE のプログラムを表す擬似コード。

function directedE()
for ( i を 1 から N まで 1 ずつ増やす ) // 各点での未探索の辺の番号を初期化
current[i] ← edgefirst[i]
endfor
top ← 1 // 探索済の経路の辺の格納位置を初期化
last ← M // 確定済の経路の辺の格納位置を初期化
x ← 1 // 出発点は点1
while ( ①last が 1 以上 )
if ( current[x] が ア でない )
temp ← current[x] // 点xからたどる接続辺はtemp
searched[top] ← temp // 接続辺tempを探索済の経路に登録
current[x] ← イ // 点xから次にたどる未探索の辺を格納
x ← end[temp] // 接続辺tempの終点を点xにする
top ← top + 1
else
top ← ウ // 探索済の辺を遡る
temp ← searched[top] // 遡った辺はtemp
path[last] ← temp // 辺tempを確定済にする
x ← エ
last ← last - 1
endif
endwhile
endfunction

設問と解答・解説

設問1

図4中のア 〜 エ に入れる適切な字句を答えよ。

(1)配点 3点

模範解答

0

採点基準

知識・理解度(内容)2点
  • 2点: 未探索の辺がないことを示す適切な値(0)が記述されている。
  • 1点: 未探索の辺がない状態についての理解は伺えるが、値の指定に誤りがある。
  • 0点: 未探索の辺がないことを示す適切な値が記述されていない。
論理性(構造)1点
  • 1点: 「0」と論理的に正しい形で記述されている。
  • 0点: 論理的に正しい形で記述されていない。

解説

【問題の本文・図表】の「図4 関数 directedE のプログラム」における条件分岐 if ( current[x] が ア でない ) を考える。

current[x] には表2で「点 n を始点とする未探索の辺の中で最小の番号を格納する。点 n を始点とする未探索の辺がない場合は 0 を格納する」と定められている(n を x に当てはめる)。
この if 文の条件を満たす場合(真の場合)の処理を見ると、temp ← current[x] として点 xx からたどる接続辺を取得し、さらにその先の探索を進めている。
一方、else 側(偽の場合)では、未探索の辺がないと判断して探索済の辺を遡る処理を行っている。

したがって、この if 文は「点 xx を始点とする未探索の辺が存在する場合」に探索を進めるための条件である。
未探索の辺がない場合は current[x] に 00 が格納されるため、「current[x] が 00 でない」場合が探索を進める条件となる。
よって、アには 00 が入る。

高得点のポイント

  • 未探索の辺が存在しない状態を示す配列 current の値を正確に理解していること。
  • プログラムの条件式として適切な形式で記述されていること。
(2)配点 3点

模範解答

edgenext[temp]

採点基準

知識・理解度(内容)2点
  • 2点: 「edgenext[temp]」、又はこの時点で同じ値になる「edgenext[current[x]]」「edgenext[searched[top]]」を記述している。
  • 1点: edgenext で次の接続辺を求める意図はあるが添字が誤り(例:edgenext[x]、edgenext[top])。
  • 0点: edgefirst[x]・end[temp] など。
論理性(構造)1点
  • 1点: 代入の右辺として成り立つ配列参照(上記の同値な式も可。空白・全角半角は問わない)で記述されている。
  • 0点: 構文が正しくない。

解説

【問題の本文・図表】の「図4 関数 directedE のプログラム」において、current[x] ← イ は「点 xx から次にたどる未探索の辺を格納」する処理である。

直前の処理で temp ← current[x] として現在の接続辺を変数 temp に取得している。
「図3 図1のグラフの場合の配列 edgefirst, edgenext」および本文「edgenextによって、辺2の次の接続辺が辺5であることが分かる」とあるように、ある辺 mm の次の接続辺の番号は配列 edgenext[m] に格納されている(表2)。
今、たどろうとしている辺は temp に格納されているため、点 xx から次にたどる未探索の辺(次の接続辺)は edgenext[temp] となる。
したがって、イには edgenext[temp] が入る。

高得点のポイント

  • 配列 edgefirst と配列 edgenext の関係を例示した図3を参考に、点 xx から次にたどる未探索の辺の求め方を正確に理解していること。
  • 配列 edgenext と変数 temp を用いて、代入文の右辺として適切な構文で記述されていること。
(3)配点 3点

模範解答

top - 1

採点基準

知識・理解度(内容)2点
  • 2点: topの値を1減らす処理が正しく記述できている。
  • 1点: ポインタを減少させる意図は分かるが、記述に軽微な誤りがある。
  • 0点: ポインタを遡る処理が理解できていない。
論理性(構造)1点
  • 1点: top - 1 と論理的に正しい式で記述されている。
  • 0点: 式の構造として正しくない。

解説

【問題の本文・図表】の「図4 関数 directedE のプログラム」の else 側は、未探索の辺が存在せず探索済の辺を遡る処理である。

配列 searched には「探索済の辺の番号を順番に格納」(表2)し、変数 top はその「探索済の経路の辺の格納位置」(図4)を表している。
探索を進める際は、辺を配列に登録した後に top ← top + 1 と加算して、次に格納すべき位置を指している。
遡る際は、直前に探索済として登録した辺を取り出す必要がある。top は次に格納する空き位置(最新の位置の次)を指しているため、直前に登録された辺の位置は top - 1 となる。
実際、ウの直後の処理で temp ← searched[top] として辺を取り出しているため、取り出す前に top の値を 11 減らしておく必要がある。
よって、ウには top - 1 が入る。

高得点のポイント

  • 探索済の辺を遡る際の、配列のインデックス(ポインタ)である top の更新ルールを理解していること。
  • 数式として適切な形式で記述されていること。
(4)配点 3点

模範解答

start[temp]

採点基準

知識・理解度(内容)2点
  • 2点: 「start[temp]」、又は同じ値の「start[searched[top]]」「start[path[last]]」を記述している。
  • 1点: 辺の始点へ戻す意図はあるが添字が誤り(例:start[top]、start[x])。
  • 0点: end[temp] など終点を指すもの。
論理性(構造)1点
  • 1点: 代入の右辺として成り立つ配列参照(上記の同値な式も可。空白・全角半角は問わない)で記述されている。
  • 0点: 構文が正しくない。

解説

【問題の本文・図表】の「図4 関数 directedE のプログラム」の else 側において、探索済の辺を遡り、その辺 temp を確定済にした後、現在の点 xx を更新する処理が x ← エ である。

〔一筆書きの経路を求める手順〕の説明に「終点 aa から辺 44 を遡ると、辺 44 の始点 dd で未探索の辺 77 が接続している」とあるように、辺を遡るということは、その辺の「終点」から「始点」へと戻ることを意味する。
遡った辺の番号は変数 temp に格納されており、表2の定義より、辺 mm の始点の番号は配列 start[m] に格納されている。
したがって、辺 temp を遡った後の現在の点 xx は start[temp] となる。
よって、エには start[temp] が入る。

高得点のポイント

  • 探索済の経路を遡る際に、遡った辺の始点へと現在の点 xx を戻す論理を正確に理解していること。
  • 配列 start と変数 temp を用いて、代入文の右辺として適切な構文で記述されていること。

設問1のイは,正答率がやや低かった。配列edgefirstと配列edgenextの関係を例示した図3も参考にして,点xから次にたどる未探索の辺の求め方を理解してほしい。

設問2

配点 3点

図1のグラフで関数 directedE を動作させたとき,while 文中の if 文は,何回実行されるか,数値で答えよ。

模範解答

16

解説

【問題の本文・図表】の「図4 関数 directedE のプログラム」の while ループ内で if ( current[x] が 0 でない ) という判定が行われる。

図2と「一筆書きの経路を求める手順」にあるように、グラフの探索において、全ての辺は「未探索」から一度「探索済」になり、その後「未探索の辺が接続する点まで遡る」過程で「確定済」となる。

  • if 文の条件を満たす側は、辺を「未探索」から「探索済」にして配列 searched に登録する処理である。すべての辺が一度ずつたどられるため、この処理はグラフの辺の数(MM 回)だけ実行される。
  • if 文の else 側は、辺を遡って「探索済」から「確定済」にして配列 path に登録する処理である。すべての探索済の辺が必ず一度ずつ遡られるため、この処理もグラフの辺の数(MM 回)だけ実行される。

while ループの中では、この if 判定の後、必ず上記のどちらかの処理が行われる。
したがって、if 文が実行される総回数は、「探索を進める回数」MM 回と「遡る回数」MM 回の合計である 2M2M 回となる。
図1のグラフにおいて辺の個数 MM は 88 であるため、8×2=168 \times 2 = 16 回実行される。
よって、実行回数は 1616 となる。

各選択肢の解説

※本問は数値入力形式のため、選択肢はありません。while ループの実行回数は辺の数の 22 倍となるため、これ以外の数値は誤りとなります。

設問2は,正答率が低かった。図2及び〔一筆書きの経路を求める手順〕から,グラフの各辺は,一度,探索済になってから確定済になることに注目して,if文の実行回数が辺の数の2倍になることを理解してほしい。

設問3

配点 2点

一筆書きができない強連結な有向グラフで関数 directedE を動作させたとき,探索はどのようになるかを,次の中から選べ。

ア. 探索が完了するが,配列 path に格納された経路は一筆書きの経路にならない。
イ. 探索が完了せずに終了して,配列 path に格納された経路は一筆書きの経路にならない。
ウ. 探索が無限ループに陥り,探索が終了しない。

模範解答

選択肢ア: 探索が完了するが,配列 path に格納された経路は一筆書きの経路にならない。

解説

一筆書きができない強連結な有向グラフで関数 directedE を動作させた場合を考える。

強連結であるため、どの点から出発しても必ず他の全ての点に到達する経路が存在するが、一筆書きができない(オイラー閉路を持たない)場合、全ての辺を1回ずつ通って出発点に戻ることはできない。
プログラム(図4)の動作において、

  • 条件を満たす側で探索を進め、進めなくなったら else 側で遡る。
  • 遡る処理(else 側)が実行されるたびに変数 last が 11 ずつ減少し、last が 00 になった時点(last が 1 以上 を満たさなくなった時点)で while ループは終了する。

このアルゴリズムでは、未探索の辺が残った状態で行き止まりになった場合でも、直前の点へとどんどん遡って配列 path に辺を格納していく。一度たどられた辺は必ずいつかは遡られるため、変数 last は確実に減少していく。したがって、無限ループに陥ることはなく、探索は必ず完了する。
強連結なので全ての辺が1回ずつ探索済になり、遡るたびに確定済となって配列 path に全 MM 本が格納され、last が 00 になって終了する(未探索の辺が残ったまま終了することはない)。しかし一筆書きができないグラフでは入る辺と出る辺の数が等しくない点があるため、path に並んだ辺は出発点に戻る閉じた経路にならない(ある辺の終点と次の辺の始点が一致しない箇所ができる、又は最後の辺が出発点に戻らない)。
よって、探索は完了するが、配列 path に格納された経路は一筆書きの経路にはならない。

各選択肢の解説

  • ア: 正解。たどった辺の数だけ必ず遡りが行われて last が 00 になるため探索は完了するが、一筆書きの経路にはならない。
  • イ: 関数 directedE では、遡るたびに last が確実に減少して終了条件を満たすため、探索が完了せずに異常終了することはない。
  • ウ: 上述の通り、一度たどった辺は有限であり、行き止まりになれば else 側で last が減少するため、無限ループに陥ることはない。

設問4

配点 3点

図4のプログラムは,配列 searched を配列 path に置き換えることで,使用する領域を減らすことができる。このとき,無駄な繰返しが発生しないように,下線①の繰返し条件を,変数 top と last を用いて変更せよ。

模範解答

topがlast以下

採点基準

知識・理解度(内容)2点
  • 2点: topとlastが等しい場合を含む境界値まで正確に繰返し条件を記述できている(top ≦ last、last ≧ top、top < last+1 など同値な条件も可)。
  • 1点: 終了条件の意図は理解しているが、topとlastが等しい場合を含まない(例: top < last)などの誤りがある。
  • 0点: 終了条件が理解できていない。
論理性(構造)1点
  • 1点: 変数 top と last を用いて、論理的に正しい比較条件として記述されている。
  • 0点: 指定された変数を用いていない、または比較条件の構造が不適切である。

解説

【問題の本文・図表】の「図4 関数 directedE のプログラム」における配列 searched と配列 path の利用状況を考える。

配列 searched はインデックス 11 から順番に(top を増やしながら)辺を格納していく。
配列 path はインデックス MM から逆順に(last を減らしながら)辺を格納していく。
これらを一つの配列(例えば searched を path に置き換える)で共有する場合、配列の前方からは探索済の経路が、後方からは確定済の経路が伸びてくる形になる。

前方から伸びる top(探索済の辺を格納する次の位置)と後方から伸びる last(確定済の辺を格納する次の位置)は、未探索の辺が残っている間は常に top ≦ last であり、探索済の辺が確定済の辺を上書きすることはない。
たどった辺の数 (top−1)+(M−last)(top-1)+(M-last) が MM になる、すなわち top = last+1 になった時点で、探索済の辺は既に経路の順に path[1]〜path[last] に並んでいる。以降の遡りでは、top を1減らすと top と last が同じ位置になるので、path[last] ← path[top] は同じ要素を同じ位置に書き直すだけの無駄な繰返しになる。したがって繰返し条件は top ≦ last となる(top = last のときは未探索の辺がまだ1本残っている)。境界値(top と last が等しい場合)を含めることが重要である。
よって、条件は topがlast以下 (または top <= last)となる。

高得点のポイント

  • 配列を共有した場合に、前方からのポインタ top と後方からのポインタ last がどの時点で探索完了(交差)となるかを理解していること。
  • top と last が等しい場合を含む境界値(top <= last または top が last 以下)まで正確に記述できていること。

設問4は,正答率が低かった。“topがlastより小さい”など,topとlastが等しい場合を含まない,誤った解答をした受験者が多かった。プログラム中の分岐や繰返しの条件は,その境界値まで正確に記述できているかどうかを,常に確認するようにしてほしい。

Spineチーム