Transcript Chap6-1

6章 グラフ探索
杉原厚吉.(2001).「データ構造とアルゴリズム」.
東京:共立出版.
2011/06/28
情報知能学科「アルゴリズムとデータ構造」
担当:白井英俊
本章の目的
• グラフと呼ばれる構造の頂点にある対象を「も
れなく重複もなく列挙する方法」を学ぶ
• 「木」はグラフの一種なので「木のなぞり」にお
いて、「木の頂点をすべて列挙する方法」は
すでに学習済み
• しかし、「グラフ」の方が一般的(いろいろな用
途に使える)
• 探索の基本形と合わせて、キューとスタックと
いう基本的なデータ構造を学ぶ
6.1 グラフとグラフ探索
• グラフ(graph) G: 頂点(vertex)の集合Vと
辺(edge)の集合Eの対
G = (V, E)
– 頂点の集合V={v1, v2, …, vn}―有限集合
– 辺の集合E-Vの2つの要素からなる集合(つまり、
大きさ2のVの部分集合)の集合。
例: { {v1, v2}, {v2, v3} }
グラフの具体的な例(図6.1)
グラフの書き方は一通りではない
V={v1, v2, v3,v4}
E={ {v1, v2 }, {v2 , v3}, {v1, v3 }, {v3, v4 } }
v2
v4
v4
v1
v2
v1
v3
(a)
v3
図6.1 グラフの例
(b)
グラフの用語:隣接している(adjacent)
・グラフG = (V, E) に対して、頂点viとvkが辺でつなが
れている(つまり、{vi,vk}∈E)とき、
viとvkは隣接している(adjacent)
・頂点を共有する2つの辺も、互いに隣接している、と
いう
練習問題1: (1)図6.1で隣接している辺の組をすべてあげよ。
(2)図6.1(a)と(b)で隣接している頂点の組をすべてあげ、そ
れが一致することを確かめよ。
グラフの頂点の列挙
• どのような場合に必要か?
例:「ゲームの局面の展開」のように、すべて
の頂点があらかじめ列挙されていない場合
• グラフ探索問題: 教科書p.61
「出発点となる頂点v0と関数Tが与えられたと
き、すべての頂点を列挙する問題」
T(v) :頂点v ∈Vに対してvと辺でつながれた頂点の
集合 (Tは頂点vに対し隣接する頂点集合を与える
関数)
例:三目並べのゲームの局面
局面が「頂点」
枝は「可能な指し手」
…
…
…
…
…
同様の問題(図はWikipediaより拝借)
頂点の個数は無限ではないが
非常に大きい
15パズル
グラフ探索をしたくなる理由:
チェス、将棋、碁など
何手か先(たとえば20手)まで
種々の局面を読んだ上で、最良の
「次の一手」を選択する
6.2 探索の基本形
• グラフGが、「一つの頂点v0と、任意の頂点から辺で
つながれた頂点を作るルールで指定されている」、
とする。
• ゲームに則していえば、
– 頂点v0 : 初期状態(、またはゲームのある局面)
– 頂点を作るルール: 駒を動かすルール。これによりあ
る局面から別な局面が作られる
• 頂点v0から出発してすべての頂点を列挙する基本アルゴリズ
ム: GRAPH-SEARCH (p.62)
GRAPH-SEARCH (教科書、p.62)
• 入力: グラフGを表す隣接頂点関数Tと初期
頂点v0
• 出力: Gのすべての頂点のリスト
手続き:
1. 頂点を入れるための入れ物A,B(初期値は空)
2. v0をAとBに入れる
3. Aから一つの頂点vを取り出し「T(v)に属す頂点でB
に入っていないものがあれば、AとBに入れる」
4. Aが空ならBを出力して終了。そうでなければ3へ。
3の実現方法はいろいろ---p.62の方式を次に示す
図6.3にGraph-Searchを試す
v0
vv11
ステップ2
ステップ3
v2
T(v0)={v1, v2}
v3
v5
v4
ステップ3
T(v1)={v3, v4}
v6ステップ3
T(v2)={v5}
A= X
B=
X
X
X
X X X
ステップ3
T(v5)={v6 }
ステップ4:終了
6.3 キューと横型探索
• Graph-Search の実現方法を考えよう。
ステップ3「 Aから一つの頂点vを取り出し」の部分を
どのように実現するか?
– Aに複数の頂点がある場合は、どれを取り出して
も良い…が、取り出し方によって列挙の順序が変
わる(「木のなぞり」の中間順、先行順、後行順)
– そこで、Aから頂点を取り出す規則と、頂点の列
挙の順序との関係を調べる
キュー(Queue)
•キューは次の条件を満たすデータ構造
1. 新しい情報を追加できる
2. 古い情報の一つを取り出す。そのときには、
最も古いものから順に取り出す
追加と取り出し方法が制限された「リスト構造」と
みることもできる
「先に入った情報から先に出て行く」という性質から、
FIFO(First In First Out)とも呼ばれている
例:宝くじ売り場に並んでいる人の列(図6.4)
キューについて学ぶこと
•
•
•
•
どのようなデータ構造によってキューが実現できるか
キューに固有な操作には何があるか?
キューを用いたグラフ探索はどのようなものか?
その他にどのような応用があるか?
例1:優先度付きプロセスの処理(優先度付きキュー)
これは、例えて言えば、「窓口に並んだ列でも払っ
たお金によって並ぶ順番を変えられる」、というような
システム
例2:A*アルゴリズム(ゲームで評価点により、次の局面を
選ぶ---動的に評価を行う以外は、上とほぼ同じ)
実は、みなグラフ探索とみなせる
キューのデータ構造と固有な操作
• キューの実現方法 : 配列と変数を用いる
配列: データの記憶用
変数head, tail : 配列の先頭の位置と末尾の位置を記憶。
変数max, 変数number: キューで記憶できるデータ数(配
列の大きさ)、現在記憶しているデータ数(初期値は0)
配列のheadからtailまでが、キューに記憶されているデータ
(空のときを除く)
• 固有な操作
エンキュー(enqueue) :キューにおいてデータを記憶
デキュー (dequeue) :キューにおいてデータを取出す
配列を用いた「単純な」キュー
データを記憶するのに
使われる順番
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 …
キューの最後尾の位置を記憶:tail
キューの先頭の位置を記憶:head
エンキュー(キューに記憶)により、tailの値が増える
デキュー(キューから取出し)により、headの値が増える
の部分は永久に使えなくなる!
直線を環状とみなす
データを記憶するための「場所」
• 配列は、一直線上に要素を並べたもの
見かけ上
• キューでは、先頭も末尾も動く
⇒ 一直線よりも環状に要素を並べた方が記
憶場所が有効に使える 2 1 n n-1
1
2
3
3
x
y
y = (x - 1) % n + 1
・
・
・
・
・
n
・
・
・
・
「環状」のキューの実現法(図6.5)
エンキュー
0
1
2
3
4
5
x
*
*
x
head
n
1
tail
2
3
初期設定
4
y
*
5
n
デキュー
x
*
z
「環状」のキューの実現法(図6.5)
エンキュー
x
1
2
3
4
5
1
head
tail
n-1
2
3
*
5
*
*
y
n-2
*
4
n-1
n-2
n
n
Rubyでの実現
class Queue # キューの表現
def initialize # 空のキューを作る
@data = [] # キューの中身(配列で表現)
@number = 0 # キューに入っている要素数
@head= 1 # 先頭の要素の位置
@tail= 0 # 末尾の要素の位置
@max = MaxSize # キューで記憶できる要素数の最大
end # def
attr_accessor :data, :number, :head, :tail, :max
def enqueue(x) … end # エンキュー:xをキューに格納
def dequeue … end
# デキュー:キューからの取出
end # class
enqueueとdequeueを作る(環状構造を考慮)
def enqueue(item)
if (@number < @max)
# データが追加可能ならば
@tailについての適切なプログラムを書く
@number += 1
# 記憶しているデータ数を1増やし
@data[@tail] = item
# 配列にデータを格納
else
# 配列に入りきらない場合
STDERR.print “Queue overflows.\n”
# エラーを表示
end # if
end # def
#
def dequeue
if (@number > 0)
@numberと@headを適切な値に設定し、
返す値を決める適切なプログラムを書く
else
STDERR.print "Queue is empty.\n"
end # if
end # def
キューを用いたグラフ探索
• 幅優先探索(width-first search)
もしくは 横型探索(breadth-first search)
図6.3で見た方法: Aがキューになっている
– 新しいデータは後ろに追加され、
– 格納されているデータは前から取り出される
• ゲームにおいて局面の先読みをするときなど
に有効
「二進木」の幅優先探索
Rubyのプログラム: (参考:orderTree.rb)
def breadthFirst(x) # 「答え」に入れる方法はGraph-Searchと異なる
nodes = [ ]
# 答えの記録用(「B」)
agenda = Queue.new # キュー(「A」)
agenda.enqueue(x) # ステップ2
while (agenda != [ ]) # キューがある限り繰り返す
x = agenda.dequeue # キューからデータを取り出す
next if (x == nil)
# データが nil なら次へ
nodes = nodes + [x] # 答えを記録(Bに入れる)
agenda.enqueue(x.left) # 左の子をキューに入れる
agenda.enqueue(x.right) # 右の子をキューに入れる
end # while
return nodes # 二進木のすべての頂点のリスト
end # def
「一般の木」の幅優先探索
Rubyのプログラム: (参考:treeTraverse2.rb)
def breadthFirst(x) # 「答え」に入れる方法はGraph-Searchと異なる
nodes = [ ]
# 答えの記録用(「B」)
agenda = Queue.new # キュー(「A」)
agenda.enqueue(x) # ステップ2
while (agenda != [ ]) # キューがある限り繰り返す
x = agenda.dequeue # キューからデータを取り出す
next if (x == nil)
# データがnilなら次へ
nodes = nodes + [x] # 答えを記録(Bに入れる)
for v in x.children
# x.childrenは「xの子の頂点すべて」の配列
agenda.enqueue(v) # 子をキューに入れる
end # for
end # while
return nodes
# 木のすべての頂点のリスト
end # def
Rubyでの実現(手抜き版)
Rubyの配列は、実はリストであることを前に述べた
また、キューは追加と取り出し方法が制限されたリス
トとみなせる
⇒ Rubyの配列を用いてキューを手軽に実現可能
データを記憶する場所:配列を使う
要素の追加(記憶, enqueue): push メソッド
要素の取り出し(dequeue):shiftメソッド
キューを空にする: clearメソッド
6.4 スタックと縦型探索
• Graph-Search の別な実現方法
ステップ3「 Aから一つの頂点vを取り出す」の部分
をキューではなく次のように変更する
Aから「情報を取り出すとき、最も後で入れられた
ものを取り出す」-LIFO(Last In FirstOut)
(プッシュダウン)スタック(pushdown stack)」
対比:キューは「情報を取り出すとき、最も先に入れ
られたものを取り出す」-FIFO
スタックは追加と取り出し方法が制限された「リス
ト構造」とみることもできる
スタックについて学ぶこと
• どのようなデータ構造によってスタックが実現
できるか
• スタックに固有な操作には何があるか?
• スタックを用いたグラフ探索はどのようなもの
か?
• その他にどのような応用があるか?
例1:プログラミング言語において、関数の呼
び出しの処理(再帰手続きを含む)
例2: 逆ポーランド記法で書かれた数式の処理
スタックのイメージ(図はWikipediaより)
情報の格納
情報の取り出し
スタックのデータ構造と固有な操作
• スタックの実現方法 : 配列と変数を用いる
配列: データの記憶用
変数top : データを記録する/取り出す位置を記憶。
配列の1からtopまでが、スタックに記憶されているデータ
スタックではキューと異なり、記録された要素数を記憶する
ための変数は不要。なぜだろうか?
固有な操作
プッシュ(push):スタックにおいてデータを記憶
ポップ(popup) :スタックにおいてデータを取出す
スタックの実現法
0
プッシュ
a
b
c
d
e
1
2
3
4
5
top
ポップ
スタックの延
びる方向
注意:これ以外に実装方法はある
が、ここでは教科書に従っている
Rubyでの実現
class Stack # スタックの表現
def initialize # 空のスタックを作る
@data = [] # スタックの中身(配列で表現)
@top = 0 # スタックの頂上(top)の記憶用
@max = MaxSize # 記憶可能な要素数
end
attr_accessor :data, :top, :max
def push(x) … end # プッシュ:xを記憶
def popup … end
# ポップ:データの取り出し
end # class
pushとpopupを作る
def push(item) # スタックにitemを記憶する
if (@top < @max)
適切なプログラムを書く
else
STDERR.print "Stack overflows.\n"
end # if
注意:ここでは教科書に従って、
end # def
プログラムを書くこと
def popup
# スタックからデータを取り出す
if (@top > 0)
適切なプログラムを書く
else
STDERR.print "Stack is empty.\n"
end # if
end # def
Rubyでの実現(手抜き版)
Rubyの配列は、実はリストであることを前に述べた
また、スタックは追加と取り出し方法が制限されたリ
ストとみなせる
⇒ Rubyの配列を用いてキューを手軽に実現可能
データを記憶する場所:配列を使う
要素の追加(記憶, push): push メソッド
要素の取り出し(popup):popメソッド
キューを空にする: clearメソッド
スタックを用いたグラフ探索
•深さ優先探索(depth-first search)
もしくは 縦型探索(breadth-first search)
これは「先行順」(preorder)の木のなぞりと同じも
の
逆ポーランド記法の数式の計算
数式の構文木(Chap5-1.ppt)で説明した『日本語式』の数式
手続き:
1. 数を読み込んだらスタックに積む
2. 演算子(+-*/のいずれか、これらを@で表す)
を読み込んだらスタックから二つの数をpopし(取り
出した順にa,bとする)、b @ aを計算してスタックに
積む(例:@が/で2,6の順に取り出されたなら、
6/2を計算し3をスタックに積む)
3. 読み込むものがなければスタックをpopして終了
例:13 5 4 + 3 / 4 * -
例:
top
13 5 4
+ 3
/
4 * -
0
1
2
3
4
5
= 9
= 3
= 12
= 1
例: 13 5 4 + 3 / 4 * -
逆ポーランド記法の数式の計算
• 演習問題
次の逆ポーランド記法で書かれた数式の処理
の過程を記述せよ。また結果はいくらか?
(1) 1 2 3 + + 5 * 10 /
(2) 1 2 3 4 5 6 7 8 9 - + - + - + - +
プログラム演習:逆ポーランド記法で書かれた数式を処理する
(計算をする)プログラムをかけ
6.5 探索のためのデータ構造
• 再びGraph-Searchに戻って・・・
A: 探索すべき頂点の候補の集合
幅優先探索ならキュー
深さ優先探索ならスタック
B: 木とグラフの違い:
木なら分かれた枝がその先で合流するこ
とはない
グラフならその可能性がある
GRAPH-SEARCH (教科書、p.62)
• 入力: グラフGを表す隣接頂点関数Tと初
期頂点v0
• 出力: Gのすべての頂点のリスト
T(v)は頂点v
の子の集合
手続き:
1. 頂点を入れるための入れ物A,B(初期値は空)
2. v0をAとBに入れる
3. Aから一つの頂点vを取り出し「T(v)に属す頂点で
Bに入っていないものがあれば、AとBに入れる」
4. Aが空ならBを出力して終了。そうでなければ3へ
グラフの例(図6.8から)
v2
5
2
4 v12 8
v1
1
1
v0
v3
v11
v4
2
1
v5
2
5
1
v13
7
3
6
8
1 v6
6
3
1
2
3
v10
2
v9
5
1
v7
v8
注:青字の数は辺の長さを表す。これを考慮した問題は7章で扱う
Graph-Searchの「B」のデータ構造
• グラフを対象とした場合、同じ頂点がBに入る可能
性がある
実は、Aでも同じことが起きる可能性がある
• 解決策
「B」のデータ構造としてハッシュテーブルを使う…
ハッシュだと登録済みの頂点かどうかを検査する手
間が一定時間で可能(リストだとデータ数に比例)
「A」に入れるのは「B」に登録されていないものだけにするこ
とで、 「A」の問題も解決!
• 計算量
ステップ3がグラフの辺の数に比例した手間ゆえ、辺の数を
mとすると、O(m)
グラフ探索の例:8パズル
• 15パズルだとちょっと複雑すぎるので、8-パ
ズルを考えよう。
(プログラムは 8puzzle.rb )
初期状態
6
4
8
3
5
2
目標状態
7
1
1
2
3
4
5
6
7
8
演習6.5
• 図6.8(下図)に示すグラフに対するGraph-Searchの
振る舞いを、(1)Aとしてキューを使った場合と、(2)ス
タックを使った場合のそれぞれに対して示せ。
(注:プログラムを書いたほうが簡単かもしれない)
v3
v2
v12
v1
v5
v11
v0
v10
v4
v13
v9
v8
v6
v7
注:隣接頂点関数の値は、その頂点に隣接する頂点を番号の小さいものから
大きいものという順番に並べたリストを返すとする。例T(v6) = [v5, v7]