1. ホーム
  2. スクリプト・コラム
  3. ゴラン

グラフの幅優先探索と深さ優先探索を実装するためのgo言語によるプログラミングを学ぶ

2022-02-14 07:33:54

グラフの実装

グラフは、ノードとその接続関係の集合体である。つまり、ノードを1次元配列で表現し、さらにノード間の関係を2次元配列で表現することができる。

// matrix implementation of the graph
typedef struct MGRAPH{
    nodes int[]; //nodes
    edges int[][]; //edges
}mGraph;


しかし、隣接行列に多数の0値が含まれるような実用的な問題では、疎なグラフを図に示すデータ構造の隣接連鎖表で表現することができます。

その左側がグラフの模式図、右側がグラフの隣接連鎖表である。赤文字はノードの通し番号を示し、連鎖表のノードはこのノードに接続されているノードで、例えばノード1はノード2と5に接続されている。のノードは go は、鎖の代わりに配列を使うのが簡単なので、鎖の構造は次のように書くことができます。

package main
import "fmt"
type Node struct{
	value int; //node is of type int
};
type Graph struct{
	nodes []*Node
	edges map[Node][]*Node // undirected graph of adjacency representation
}


ここで map はGo言語のキー・バリュー・インデックス型であり、以下のフォーマットで定義されます。 map[<op1>]<op2> を使用します。 <op1> は、そのキー <op2> は値です。グラフ構造では map[Node][]*Node を表します。 Node に相当します。 Node ポインタからなる配列です。

以下は、Goでグラフを生成します。

//add nodes
// can be interpreted as a member function of Graph
func (g *Graph) AddNode(n *Node) {
	g.nodes = append(g.nodes, n)
}
//add edges
func (g *Graph) AddEdge(u, v *Node) {
	g.edges[*u] = append(g.edges[*u],v) //u->v edges
	g.edges[*v] = append(g.edges[*v],u) //u->v edges
}
//print graph
func (g *Graph) Print(){
	//range traverses g.nodes, returning index and value
	for _,iNode:=range g.nodes{
		fmt.Printf("%v:",iNode.value)
		for _,next:=range g.edges[*iNode]{
			fmt.Printf("%v->",next.value)
		}
		fmt.Printf("\n")
	}
}
func initGraph() Graph{
	g := Graph{}
	for i:=1;i<=5;i++{
		g.AddNode(&Node{i,false})
	}
	//generate edges
	A := [...] int{1,1,2,2,2,3,4}
	B := [...] int{2,5,3,4,5,4,5}
	g.edges = make(map[Node][]*Node)//initialize edges
	for i:=0;i<7;i++{
		g.AddEdge(g.nodes[A[i]-1], g.nodes[B[i]-1])
	}
	return g
}
func main(){
	g := initGraph()
	g.Print()
}


その実行結果は

PS E:\Code> go run . \goGraph.go
1:2->5->
2:1->3->4->5->
3:2->4->
4:2->3->5->
5:1->2->4->


BFS

Breadth-first search (BFS) は、グラフの元ノードが与えられると、その外側を仮探索する最も単純なグラフ探索アルゴリズムである。つまり、探索元ノードから距離k kのノードを探索し、探索元ノードから距離k + 1 k+1 k+1のノードを得た後、初めて探索が継続される、という特徴を持っています。

グラフ検索では、1が2を検索し、それに応じて2が1を検索するといった重複の問題が発生する可能性がある。そこで、グラフのノードに対して searched という値でマークします。 false は検索されていないことを意味し、それ以外は検索されていることを意味する。

type Node struct{
	value int;
	searched bool;
}
/*func initGraph() Graph{
    g := Graph{}
*/
    //Change the node generation function accordingly
    for i:=1;i<=5;i++{
		g.AddNode(&Node{i,false})
	}
/*
...
*/


また、検索処理中にノードの属性が変更されるため map のハッシュも変更される、すなわち Node をキーにすると、元の隣接ノードに対応しなくなるので Graph グラフ内のエッジのキー値をノードへのポインタに置き換えることで、ノードの値が変化してもポインタは変化しないようにした。

type Graph struct{
	nodes []*Node
	edges map[*Node][]*Node // undirected graph of adjacency representation
}
//add edges
func (g *Graph) AddEdge(u, v *Node) {
	g.edges[u] = append(g.edges[u],v) //u->v edges
	g.edges[v] = append(g.edges[v],u) //u->v edges
}
//print graph
func (g *Graph) Print(){
	//range iterates over g.nodes, returning index and value
	for _,iNode:=range g.nodes{
		fmt.Printf("%v:",iNode.value)
		for _,next:=range g.edges[iNode]{
			fmt.Printf("%v->",next.value)
		}
		fmt.Printf("\n")
	}
}
func initGraph() Graph{
	g := Graph{}
	for i:=1;i<=9;i++{
		g.AddNode(&Node{i,false})
	}
	//generate edges
	A := [...] int{1,1,2,2,2,3,4,5,5,6,1}
	B := [...] int{2,5,3,4,5,4,5,5,6,7,8,9}
	g.edges = make(map[*Node][]*Node)//initialize edges
	for i:=0;i<11;i++{
		g.AddEdge(g.nodes[A[i]-1], g.nodes[B[i]-1])
	}
	return g
}
func (g *Graph) BFS(n *Node){
	var adNodes[] *Node // store the nodes to be searched
	n.searched = true
	fmt.Printf("%d:",n.value)
	for _,iNode:=range g.edges[n]{
		if !iNode.searched {
			adNodes = append(adNodes,iNode)
			iNode.searched=true
			fmt.Printf("%v ",iNode.value)
		}
	}
	fmt.Printf("\n")
	for _,iNode:=range adNodes{
		g.BFS(iNode)
	}
}
func main(){
	g := initGraph()
	g.Print()
	g.BFS(g.nodes[0])
}


図は

出力は

PS E:\Code\goStudy> go run . \goGraph.go
1:2->5->9->
2:1->3->4->5->
3:2->4->
4:2->3->5->
5:1->2->4->6->7->
6:5->8->
7:5->
8:6->
9:1->
// The following are the BFS results
1:2 5 9
2:3 4
3:
4:
5:6 7
6:8
8:
7:
9:


DFS

深さ優先探索(DFS)とBFSの違いは、後者の探索処理が、最初に探索するノードを親ノードとし、そのノードに接続するノードを第一世代ノードとし、第一世代ノードの後に第二世代ノードを探索するという、レイヤーごとの処理と理解できる点である。そして、未探索のノードがなくなるまで、すべてのノードを繰り返し、別の系統を探します。

基本的な手順は以下の通りです。

  • 最初に未訪問の頂点 V 0 V_0 V0 を初期頂点として選択し、訪問済みとしてマークする。
  • 次に、V 0 V_0 V0 に隣接するすべての頂点を検索して訪問済みかどうかを判断し、未訪問の頂点があれば任意の1頂点を選択して V 1 V_1 V1 を訪問し、V n V_n Vn に未訪問ノードがなくなるまでこれを繰り返す。
  • グラフ内にまだ訪問していない頂点がある場合は、さらに1つの頂点を選んで訪問し、それ以外の場合は探索を終了する。

まず、第二段階として、一つのノードに対する最深部探索の結果を実装します

func (g *Graph) visitNode(n *Node){
	for _,iNode:= range g.edges[n]{
		if !iNode.searched{
			iNode.searched = true
			fmt.Printf("%v->",iNode.value)
			g.visitNode(iNode)
			return
		}
	}
}
func main(){
	g := initGraph()
	g.nodes[0].searched = true
	fmt.Printf("%v->",g.nodes[0].value)
	g.visitNode(g.nodes[0])
}


その結果は

PS E:\Code> go run . \goGraph.go
1->2->3->4->5->6->8->


すなわち

ご覧のように、まだアクセスされていないノード7と9があります。

完全なDFSアルゴリズムは、一点探索の前に全ノードの探索を追加するだけです

func (g *Graph) DFS(){
	for _,iNode:=range g.nodes{
		if !iNode.searched{
			iNode.searched = true
			fmt.Printf("%v->",iNode.value)
			g.visitNode(iNode)
			fmt.Printf("\n")
			g.DFS()
		}
	}
}
func main(){
	g := initGraph()
	g.nodes[0].searched = true
	fmt.Printf("%v->",g.nodes[0].value)
	g.visitNode(g.nodes[0])
}


その結果は

PS E:\Code> go run . \goGraph.go
1->2->3->4->5->6->8->
7->
9->


上記は、広さと深さの最初の検索の詳細のグラフを実装するために学習するGo言語のプログラミングは、広さと深さの最初の検索のグラフのGo言語の実装についての詳細な情報は、スクリプトホーム他の関連記事に注意を払うしてください