アルゴリズムからプログラムへ GRAPH-SEARCH 情報知能学科 白井 英俊 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の方式を次に示す グラフの表現 グラフGは頂点の集合Vと枝の集合Eの組合せ V={v1, v2, v3,v4} E={ {v1, v2 }, {v2 , v3}, {v1, v3 }, {v3, v4 } } ここでは頂点v0から始めてVを求めることが目的。Gは 上記の形で与えられてはおらず、 「グラフGを表す隣接頂点関数Tと初期頂点v0」で与え られている ⇒ 頂点ごとに、それにTという関数によって隣接する頂 点の集まりが得られる、と考える ひとつの表現方法 V={v1, v2, v3,v4} E={ {v1, v2 }, {v2 , v3}, {v1, v3 }, {v3, v4 } } に対しては、 T(1)={2, 3} T(2)={1, 3} T(3) ={1,2,4} T(4)={3} という関数Tを作る v2 v4 v1 v3 集合と配列 たとえば T(1)={2, 3} をRubyで表現(実現)することを考えよう。 Rubyでは「集合」を表現することはできない(そうういう データ構造を持っていない) しかし、「データの集まり」を表すには「配列」でもよさそ う。。。 とすると、E={ {v1, v2 }, {v2 , v3}, {v1, v3 }, {v3, v4 } }は T=[nil, [2,3], [1,3], [1,2,4], [3] ] で表現できそう T(1) T(2) T(3) T(4) 別な方法 今までの講義では、木をClassを用いて表してきた 今示した配列を使う方法とは相容れない… そこで、グラフもClassを用いて表すことを考えよう class GraphNode 一つ一つの頂点を表現 def initialize(x="") @label=x 頂点のラベル @edges=Array.new 隣接頂点を要素とする配列 end # def Classによるグラフの表現 Classを用いてグラフの頂点を表すとすれば、 頂点v1に対する隣接頂点関数の値 T(v1)は、 v1.edges v で与えられる。つまり、 v1.edges ⇒ [v2, v3] 2 v1 注意: edgeとは「辺」のこと。でも上では 隣接頂点を要素とする配列が得られる。 このままだと名前が誤解を与えかねないので、 adjacentNodes (実質はedgesと同じ) v3 によって隣接頂点関数(T)の代わりをさせることとする つまり、上の例では v1.adjacentNodes によってv1の隣接頂点が得られる v4 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の方式を次に示す 関数graphSearch アルゴリズムの記述から def graphSearch(v0) (1) 入れ物A, Bを用意する (2) 入れ物A, Bにv0を入れる (3) Aから一つの頂点vを取り出し「T(v)に属す頂点 でBに入っていないものがあれば、AとBに入れる」 (4) Aが空ならBを出力して終了。そうでなければ(3)へ end という大まかな構造が見える (1)入れ物A, Bを用意する プログラムにおいて「入れ物」とは何か? 答え:データを記憶するための変数、配列、など のこと ただし、変数で記憶できるのは高々1つ 配列やハッシュ、スタック、キューなど、いろいろ な要素を記憶できるものが「入れ物」と呼ぶの にふさわしい (1)入れ物A, Bを用意する(続き) ここでは、Aにはスタックかキューのどちらかを使う(指定 済み) Bは指定がないので、配列、ハッシュ、…なんでもよい? なぜ重要かは(3)で説明 どういう用途の「入れ物」かが選択の鍵 (3)で「Bに入っているかどうか」が重要 ⇒Bの中の要素を「すばやく」検索できるデー タ構造が向いている⇒それならハッシュ! でも、どうやって「用意する」? (1)入れ物A, Bを用意する(続き) • 配列、ハッシュ、キュー、スタックなどの「デー タ構造」は、newメソッドでインスタンスを作る • 例 配列: Array.new ハッシュ: Hash.new スタック: Stack.new キュー: Queue.new 木: … これらはRubyの組み込み (最初からある)のデータ構造 これらはRubyの組み込みではない 別途Classの宣言が必要 しかし、newによってインスタンスを 作ることは同じ キューとスタックの準備 • キューのクラス queueX.rb class Queue attr_accessor :head, :tail def enqueue(item) … end def dequeue … end def empty … end def find(item) … end end • スタックのクラス stackX.rb class Stack attr_accessor :data, :top def push(item) … end def popup … end def empty … end end GRAPH-SEARCHプログラム(queue版) def graphSearchQ(v) # キューを用いたグラフ探索 check = Hash.new(false) # 頂点が記録済みかどうかのチェック用(B) agenda = Queue.new # 調査すべき頂点の記憶用(A) agenda.enqueue(v) # 始点vをAに入れる(enqueue) result = [v] # 始点vを訪問リストに入れる check[v] = true #始点vを記録済みとしてチェック while (! agenda.empty) # 調査すべき頂点がある限り繰り返し v = agenda.dequeue # 次に調査すべき頂点(v)を取出す for w in v.adjacentNodes # vに隣接する頂点それぞれ(w)に対し if (! check[w]) # wが記録済みでなければ agenda.enqueue(w) # wを調査すべき頂点に登録(A) check[w]=true # wを記録済みとして登録(B) result << w # wを訪問リストの末尾に入れる end # if end # for end # while return result # (すべての頂点を調査したので) 訪問リストを返す end # def 関数graphSearch アルゴリズムの記述から def graphSearch(v0) (1) 入れ物A, Bを用意する (2) 入れ物A, Bにv0を入れる (3) Aから一つの頂点vを取り出し「T(v)に属す頂点 でBに入っていないものがあれば、AとBに入れる」 (4) Aが空ならBを出力して終了。そうでなければ(3)へ end という大まかな構造が見える (2) 入れ物A, Bにvを入れる 入れ物とは、配列、ハッシュ、スタック、キュー等のこと これにvを入れるとは? 配列の場合:インデックスを指定するか、後に付加えるのが普通。 例:arrを配列、iをインデックス用の変数とすれば、 arr[i]=v や arr << v など。 ハッシュの場合:vそのものをインデックスとして、値にnil以外を 与える. 例: h をハッシュとすれば、 h[v] = true 参考: ハッシュの場合、「入っていないもの(たとえばw)を参照 しようとするとnilが値となる. h[w] -> nil では、スタックやキューに入れるには? (2) 入れ物A, Bにvを入れる(続) スタックやキューに入れるには? そう、もう学んだこと… スタック: stをスタックとすれば、st.push(v) キュー: qをキューとすれば、 q.enqueue(v) 質問: スタックやキューの中のものを取り出すには? 答: スタック: stをスタックとすれば、st.popup キュー: qをキューとすれば、 q.dequeue GRAPH-SEARCHプログラム(queue版) def graphSearchQ(v) # キューを用いたグラフ探索 check = Hash.new(false) # 頂点が記録済みかどうかのチェック用(B) agenda = Queue.new # 調査すべき頂点の記憶用(A) agenda.enqueue(v) # 始点vをAに入れる(enqueue) result = [v] # 始点vを訪問リストに入れる check[v] = true #始点vを記録済みとしてチェック while (! agenda.empty) # 調査すべき頂点がある限り繰り返し v = agenda.dequeue # 次に調査すべき頂点(v)を取出す for w in v.adjacentNodes # vに隣接する頂点それぞれ(w)に対し if (! check[w]) # wが記録済みでなければ agenda.enqueue(w) # wを調査すべき頂点に登録(A) check[w]=true # wを記録済みとして登録(B) result << w # wを訪問リストの末尾に入れる end # if end # for end # while return result # (すべての頂点を調査したので) 訪問リストを返す end # def 関数graphSearch アルゴリズムの記述から def graphSearch(v0) (1) 入れ物A, Bを用意する (2) 入れ物A, Bにv0を入れる (3) Aから一つの頂点vを取り出し「T(v)に属す頂点 でBに入っていないものがあれば、AとBに入れる」 (4) Aが空ならBを出力して終了。そうでなければ(3)へ end という大まかな構造が見える (3) Aから一つの頂点vを取り出し... Aにはスタックかキューを使う(指定済み) Aから要素を取り出してそれをvとする、ということ Aがスタックの場合(もうわかっていますね?) スタックから要素を取り出すには… だからstをスタックとすれば v= Aがキューの場合 穴埋め! キューから要素を取り出すには… だからqをキューとすれば v= …「T(v)に属す頂点でBに入っていないも のがあれば、AとBに入れる」 (3)の前半でAから取り出された頂点をv‘とし、T(v’)の一つの要素 をvとしておく vがBに入っているかいないかは、どうやってわかる? Bはハッシュを使うことにした(スライド11枚目) …それは、検索が速いから。でもどうやって? 答え:hをハッシュとすれば h[v]の値でわかる 説明: hに要素(例えばw)を入れる時に、 h[w]=true として値を設定。だから「true以外」の 値が返されれば、「入っていない」ことがわかる ここで、条件式について一言 条件式は、 if 条件式 … elsif 条件式 … end や、 while 条件式… end などで用いられる Rubyの場合、条件式がtrueを返すと、その条件式が 「成り立った」ことになり、たとえばifではその次のプロ グラムが、whileではendまでのプログラムが実行され る たとえば、 x==3はxが3ならtrue、そうでなければfalseを 返す 条件式の値がfalse(やnil)の場合、その条件式は「成り 立たなかった」ということになる vがBに入っているかいないか なぜこれが重要なのだろうか? このチェックをしないプログラムを書いた人から のコメント:「キューにものが入りすぎてQueue overflowが表示されてしまう」 グラフは(木と異なり)頂点から枝をたどっていくと 、その頂点に戻る可能性がある。つまり迷路 でいえば同じところをぐるぐる回ってしまう可能 性がある。これを何とかしたい! → 頂点をAにいれるための条件として「すでに訪れたことがない」 (Bに入っていない)とすればよい (3)をまとめて書くと。。。 確認: 「T(v)に属す頂点でBに入っていないものがあれば、 AとBに入れる」は大丈夫ですね?(「AとBに入れる」方 法はもうわかっている?) v.adjacentNodesでT(v)の代わりをする 以下ではAをキューとします(スタックの場合は自分で書い てみてください) A(キューqとする)から一つの頂点v1を取り出し v1 = q.dequeue 「T(v1)に属す頂点vでB(hとする)に入っていないものがあれ ば、AとBに入れる if (h[v1.adjacentNodes] != true) q.enqueue(v.adjacentNodes); おっと!v.adjacentNodesは配列だ! h[v.adjacentNodes]=true end (3)をまとめて書くと。。。 ちょっと焦りすぎました 頂点vに隣接する頂点集合が v.adjcentNodes (T(v)と同じ)で得られるのですが、これは頂点を 要素とする配列を返す これをそのままAやBに入れてはいけません(A やBに入れるのは「頂点」と決めているので) ここは「繰り返し」(forやwhile)を使って、 v.adjcentNodesの要素を一つ一つBにないこと を確認して次々AやBに入れる必要があります GRAPH-SEARCHプログラム(queue版) def graphSearchQ(v) # キューを用いたグラフ探索 check = Hash.new(false) # 頂点が記録済みかどうかのチェック用(B) agenda = Queue.new # 調査すべき頂点の記憶用(A) agenda.enqueue(v) # 始点vをAに入れる(enqueue) result = [v] # 始点vを訪問リストに入れる check[v] = true #始点vを記録済みとしてチェック while (! agenda.empty) # 調査すべき頂点がある限り繰り返し v = agenda.dequeue # 次に調査すべき頂点(v)を取出す for w in v.adjacentNodes # vに隣接する頂点それぞれ(w)に対し if (! check[w]) # wが記録済みでなければ agenda.enqueue(w) # wを調査すべき頂点に登録(A) check[w]=true # wを記録済みとして登録(B) result << w # wを訪問リストの末尾に入れる end # if end # for end # while return result # (すべての頂点を調査したので) 訪問リストを返す end # def 関数graphSearch まだ完成ではありません。(4)を忘れています。 def graphSearch(v0) (1) 入れ物A, Bを用意する (2) 入れ物A, Bにv0を入れる (3) Aから一つの頂点vを取り出し「T(v)に属す頂点 でBに入っていないものがあれば、AとBに入れる」 (4) Aが空ならBを出力して終了。そうでなければ(3)へ end という大まかな構造が見える (4) Aが空ならBを出力して終了。そうでな ければ(3)へ (4)を忘れていました。ここでは二つ大事なことがあります 1. どういう値を返すか … このアルゴリズムではB(ハッ シュを使っている)を返すことになっている 2. 「そうでなければ(3)へ」はどうするか? これは「繰り返し」を意味する 1に対しては、hをハッシュとすれば、h.keysによって、 記憶された頂点を配列にして返してくれます(?!) 2に対しては、A(スタックかキュー)が空かどうかを判定 するメソッドを使います(そのためにemptyというメソッド をあらかじめ定義しておいた) (4) Aが空ならBを出力して終了 「何を返すか」は本当は大事な問題 B(ハッシュhとする)に入っている頂点の集合を 返すだけなら、h.keys でよいが。。。 ここでは、どの頂点がどういう順番で入ってきた かも示したい(と思いません?) だから、resultという配列を別に用意して、それを 返すようにした 最終形は次のスライド… GRAPH-SEARCHプログラム(queue版) def graphSearchQ(v) # キューを用いたグラフ探索 check = Hash.new(false) # 頂点が記録済みかどうかのチェック用(B) agenda = Queue.new # 調査すべき頂点の記憶用(A) agenda.enqueue(v) # 始点vをAに入れる(enqueue) result = [v] # 始点vを訪問リストに入れる check[v] = true #始点vを記録済みとしてチェック while (! agenda.empty) # 調査すべき頂点がある限り繰り返し v = agenda.dequeue # 次に調査すべき頂点(v)を取出す for w in v.adjacentNodes # vに隣接する頂点それぞれ(w)に対し if (! check[w]) # wが記録済みでなければ agenda.enqueue(w) # wを調査すべき頂点に登録(A) check[w]=true # wを記録済みとして登録(B) result << w # wを訪問リストの末尾に入れる end # if end # for end # while return result # (すべての頂点を調査したので) 訪問リストを返す end # def 出欠課題(その2) 本日のウェブページから、graphSearch.rb をダウンロー ドする そこには queue版のGraph-Searchプログラムが載って いる (1) それを動かしてみる。v6を出発点とした時の頂点の リストが表示されるので、自分の解答と比較する(最 後にエラー表示されるが無視すること) (2) queue版にならってstack版を作る(graphSearch.rbの graphSearchS関数を完成させる) (3) 完成したら、v6を出発点とした時の頂点のリストが 表示されるので自分の解答と比較する
© Copyright 2024 ExpyDoc