Transcript visited

コンピュータソフトウェア
8. グラフの探索とその応
用
田浦
http://www.logos.ic.i.u-tokyo.ac.jp
/~tau/lecture/software/
以下の様々な問題が似たような方
法で効率的に解ける






ある頂点sから到達可能な(s * vとなる)頂点vをすべて列挙
する
無向グラフの連結成分への分解(各連結成分に含まれる頂
点を列挙)
無向グラフの各連結成分の全域木を(ひとつ)見つける
DAGのトポロジカルソート
有向グラフの閉路があるかどうかを検査し,あれば見つける
有向グラフの強連結成分への分解
着眼:
どの問題も,「ある頂点を基点としてそこから
到達可能な頂点をすべて発見(訪問,到達)す
る」という手続き(頂点の探索)の応用
注: トポロジカルソートとは




有向グラフの全頂点を以下の条件を満たすように一列に(v1;
v2; ...; vn)ならべる
 条件: vi  vj ならば,i < j
グラフに閉路がある場合,明らかにこのような並べ方は存在
しない
閉路がない場合(DAG)は,(一般には複数通り)存在する
数学的な言い換え:
 ある半順序関係  を拡張する全順序 < の構築
1
4
3
2
6
5
1;3;2;4;6;5
1;3;4;6;2;5
トポロジカルソートの応用例


例1
 頂点 : 教える項目,辺 a  b : 項目bを教えるにはaを事
前に教えておく必要がある
 トポロジカルソート : あの人下手といわれない教える順番
例2
 頂点 : ある評価したい(値を求めたい)式の部分式
 トポロジカルソート(の逆順) : 式を評価すべき順番
*
(a[i]+b[i])(a[i]+b[i]+c[i])
*; +1; +2; a[i]; b[i]; c[i]
+2
+1
a[i]
c[i]
b[i]
一頂点からの深さ優先探索(depth first
search; DFS)とその自然なコード


入力: グラフGとその一頂点(開始点) s
目的: sから到達可能な頂点をすべて見つける
 ここではそのような頂点を一度ずつ表示(print)している
u  vなる頂点v(隣接ノード)のリスト
dfs_visit(u, G, visited) {
print(u);
visited[u] = 1;
for v  adjacents(u) {
if (visited[v] == 0) then
dfs_visit(v, G, visited)
}
}
dfs_visit_from(s, G) {
visited = new int[n];
for (i = 0; i < n; i++) visited[i] = 0;
dfs_visit(s, G, visited);
}
基本的な性質

visited[v]がすべて0の状態でdfs_visit(s, G, visited) を呼ぶと,
sから到達可能なノードすべておよびそれらのみに対し,
visiteid[v]=1となって終了する
深さ優先による全頂点の探索
dfs_visit(u, G, visited) {
print(u);
visited[u] = 1;
for v  adjacents(u) {
if (visited[v] == 0) then
dfs_visit(v, G, visited)
}
}
dfs_visit_all(G) {
visited = new int[n];
for (i = 0; i < n; i++) visited[i] = 0;
for (i = 0; i < n; i++) {
if (visited[i] == 0) {
dfs_visit(i, G, visited);
}
}
全頂点探索の計算量


時間計算量 : O(n + m)
 n : 頂点数, m : 辺数
空間計算量: O(n) (visited配列の大きさ)
無向グラフの場合の挙動


FACT: sから到達可能な全ノード = sを含む連結成分(無向グ
ラフの性質)
 深さ優先探索を用いて連結成分への分解が求まる
FACT: 再帰呼び出しの木 = sを含む連結成分の全域木
 連結無向グラフの全域木(のひとつ)が求まる
8
1
3
9
7
2
6
0 s
5
4
無向グラフの連結成分への分解
dfs_visit(u, G, visited, C) {
C.add(u);
visited[u] = 1;
for v  adjacents(u) {
if (visited[v] == 0) then
dfs_visit(v, G, visited , C)
}
}
connected_components(G) {
visited = new int[n];
for (i = 0; i < n; i++) visited[i] = 0;
for (i = 0; i < n; i++)
if (visited[i] == 0) {
C = {} /* empty set of nodes */
dfs_visit(i, G, visited, C);
print C;
}
}
無向グラフの各連結成分の全域木
dfs_visit(u, G, visited, E) {
visited[u] = 1;
for v  adjacents(u) {
if (visited[v] == 0) then
E.add(u  v)
dfs_visit(v, G, visited , E)
}
}
spanning_trees(G) {
visited = new int[n];
for (i = 0; i < n; i++) visited[i] = 0;
for (i = 0; i < n; i++)
if (visited[i] == 0) {
E = {} /* empty set of edges */
dfs_visit(i, G, visited, E);
print E;
}
}
無向グラフに対する閉路の検出

無向グラフの性質: 木でない  閉路がある
 深さ優先探索中に visited[v] == 1なる辺 u  v (全域木に
含まれない辺)が見つかったら閉路が存在する
dfs_visit(u, G, visited) {
visited[u] = 1;
for v  adjacents(u) {
if (visited[v] == 0) then
dfs_visit(v, G, visited);
else
print “a cycle is found!”
}
} 練習:閉路を具体的に求める(表示する)
ようにプログラムを修正せよ
有向グラフの場合の挙動


sを含む強連結成分 (の頂点の集合)
 sから到達可能
(な頂点の集合)
 sを含む連結成分 (の頂点の集合)
どの二つも一般には一致しない
sを含む強連結成分
s
s
sから到達可能
sを含む連
結成分
有向グラフの閉路の検出

無向グラフと同じ手は使えるか?
/* ???? */
dfs_visit(u, G, visited) {
visited[u] = 1;
for v  adjacents(u) {
if (visited[v] == 0) then
dfs_visit(v, G, visited);
else
print “a cycle is found!” (*)
}
}
○ 閉路がある  (*)が実行される
× (*)が実行される  閉路がある
残念!
修正

アイデア: dfs_visit(u, ...)の実行中に再び,uに到達したら閉
路

以下が正しい(閉路がある場合,およびそのときに限り(*)を
実行する)か?
dfs_visit(u, G, state) {
state[u] = visiting;
for v  adjacents(u) {
if (state[v] == unvisited) then
dfs_visit(v, G, visited);
else if (state[v] == visiting)
print “a cycle is found!”; (*)
}
state[u] = visited;
}
find_cycle(G) {
state = new STATE[n];
for (i = 0; i < n; i++) state[i] = unvisited;
for (i = 0; i < n; i++)
if (state[i] == unvisited)
dfs_visit(i, G, state);
}
証明


(*)が実行される  閉路がある
 ほとんど自明
閉路がある  (*)が実行される
 サイクルをa1  ...  am , この中で最初に呼び出された
(visitingになった)頂点を一般性を失うことなくa1とする
 プログラムの作り(再帰呼び出し)よりa1に対する呼び出し
が終了する(visitedになる)前にa2 ,..., amはすべてvisitedに
なる
 特にamへの呼び出し内で辺am  a1が発見され,(*)が実
行される
DAGのトポロジカルソート

深さ優先探索で,全頂点に対する呼び出しが終了する
(state[i] = visitedが実行される)順番の逆順がトポロジカル
ソートになっている
dfs_visit(u, G, state, Q) {
state[u] = visiting;
for v  adjacents(u) {
if (state[v] == unvisited) then
dfs_visit(v, G, visited);
else if (state[v] == visiting)
print “not a DAG!”; (*)
}
state[u] = visited;
Q.enque(u); /* add to tail */
}
topological_sort(G) {
state = new STATE[n];
for (i = 0; i < n; i++) state[i] = unvisited;
/* Java LinkedList, C++ dequeue, etc. */
Q = empty_queue();
for (i = 0; i < n; i++)
if (state[i] == unvisited)
dfs_visit(i, G, state, Q);
return reverse(Q);
}
証明


u  v とする
dfs_visit(u, ...)中で
 visited[v] == unvisited  vに対する再帰呼び出しが行わ
れ,dfs_visit(v, ...)はdfs_visit(u, ...)に先立って終了する
 visited[v] == visiting : DAGなのでありえない
 visited[v] == visited  dfs_visit(v, ...)はすでに終了してい
る
有向グラフの連結成分・強連結成
分への分解


連結成分への分解
 辺の向きを無視した無向グラフを作り,それを連結成分へ
分解すればよい
強連結成分への分解
 少し難しい
 時間がないので省略(石畑の教科書参照)
深さ優先以外の探索


目的は同じ(ある頂点から到達可能な全
頂点をすべて発見・訪問する)
 頂点を訪問する「順序」が違う
一般には訪問済み頂点に隣接したどの
頂点も次に訪問可能
深さ優先探索が選ぶ頂点:
根からの枝数(深さ)が最
大のもの
当然こちらを先に
選ぶことも可能
探索の一般形
最前線: 訪問済みから1 hop
visit(s, G, visited) {
F = { s }; /* Frontier */
visited[s] = 1;
while (F  {}) {
u = F.delete_one();
for v  adjacents(u) {
if (visited[v] == 0) {
visited[v] = 1;
F.add(v);
}
}
ここでどのノードを選ぶか
}
(だけ)で探索順序が決まる
}
visited[v] = 1 
訪問済み
訪問済みから>1 hop
or
様々な探索戦略



深さ優先探索(depth first search; DFS)
 Fの中で最後にFに入ったものを選ぶ
 Fをstack (前から入れて前から出す; LIFO)にする
幅優先探索(breadth first search; BFS)
 Fの中で,探索開始頂点sからの深さ(通った枝の本数)が
最小のものを選ぶ
 Fをqueue (後ろから入れて前から出す; FIFO)にする
一般の優先度探索
 ある(目的に応じた)判断基準で最小・最大のものを選ぶ
 Fを優先度キューとする
幅優先探索の性質

幅優先探索によってたどられた辺(visited[v] = 1の実行を引
きおこしたu  v)の集合は,sから各頂点への最短路(最小の
辺数で到達する道)を与える
 「重みなしグラフ(重み一律)の最短路の問題」
bfs_visit(s, G, visited) {
F = queue(s); /* C++ deque/list, Java : LinkedList */
visited[s] = 1;
while (not F.is_empty()) {
9
u = F.deq(); /* 前から出す */
for v  adjacents(u) {
if (visited[v] == 0) {
6
visited[v] = 1;
F.enq(v); /* 後ろに入れる */
}}}}
1
4
7
3
8
0
2
5
探索問題


抽象的(暗黙的)なグラフ:
 グラフの表現そのものがデータ構造としては作られないようなグラフ
(えてして巨大)
 点(u)からその隣接点の集合(adjacents(u))が計算できれば,なんでも
「グラフ」と見ることが可能
 例: ゲームのグラフ
 頂点: ゲームの状態,u  v : 状態uから状態vへ「一手で」移ること
が可能
これまで述べた探索アルゴリズムはこのような抽象的なグラフに対しても
適用可能
 巨大な状態空間の中からある性質を持った「ゴール・解」の「探索」
  このような問題をしばしば「探索問題」と呼ぶ
練習問題

グラフ探索の考え方を使って,「8パズル」を解くプログラムを
書いてみよ
グラフのノード数(の上界)は?
2 3 4
連結(どこからでもゴールできる)か?
最短(最小手数)でゴールするには?
7 1 5
8
1
2
3
4
5
6
7
8
6
ゴール
2
3
4
7
1
5
8
6
探索問題の困難と戦略
困難
 探索空間が巨大(頂点数nが大きい)
 すべてを探索するには時間がかかりすぎる
 メモリに収まりきらない(visited配列すべてを記憶できない)
 目的はある特定の頂点(ゴール,解)を見つけることで,全頂点
を見る必要は(運がよければ)ない
 ゴールに早くたどり着く戦略(経験則; heuristics)が必要

いくつかの経験則・戦略


隣接頂点 adjacents(u)の順序付け
 答えに到達する可能性が高い頂点を先に探索
 頂点の「有望度」(例: 解への近さ)を測る経験則が必要
メモリの節約
 深さ優先探索で訪問済み頂点(visited)を記憶しない
 グラフが木である(問題の性質から,2度同じ頂点を訪
問する可能性がない)場合に有用
 浅く広い木の場合になお有用(必要メモリ量O(深さ))
 同じ頂点を2度探索することが許容される場合も安全
反復深化探索


深さ制限つき深さ優先探索
 深さ優先探索を,開始点からの距離(再帰呼び出しを使うならその呼
び出しの深さ)に制限をつけて行う
反復深化
 その深さ制限を徐々に増やしながら深さ優先探索を繰り返す
 利益:
 深さ優先と同じメモリ量
 visited配列を記憶しなくても,無限ループはしない
 幅優先に似た広く浅い探索
 浅い探索の結果を,「解の有望度」として,深い探索の順序制御に
用いることができる
挑戦しがいのある課題

15パズル,ルービックキューブなど,状態空間が大きいパズ
ルの解法
 ルービックキューブくらいなら課題2として良いかも