C++ Boost

A Quick Tour of the Boost Graph Library

いくつかの点において、グラフのデータ構造とアルゴリズムという領域はコンテナのそれよりずっと複雑である。 STL で用いられている抽象的イテレータというインタフェースは、 グラフアルゴリズムがグラフを渡る(traverse)無数の方法を網羅するのに十分なほど高機能とは言えない。 我々はそれに代わる抽象的インタフェースを定式化した。 このインタフェースは、イテレータが基本的なコンテナで為すことと同じ用途をグラフに提供する (イテレータも未だに大きな役割を演じているが)。 Figure 1 は STL と BGL の間の類似性を表している。

 

Figure 1: The analogy between the STL and the BGL.

 

グラフの抽象化は頂点(またはノード)の集合と、頂点に接続された辺(またはアーク)の集合からなる。 Figure 2 は(0 から 4 のラベルを振られている) 五つの頂点と 11 の辺を持つ有向グラフを表す。 頂点を出発した(leaving)辺はその頂点の 出辺 と呼ばれる。 {(0,1),(0,2),(0,3),(0,4)} の辺はすべて頂点 0 の出辺のである。 頂点に入ってくる(entering)辺はその頂点の 入辺 と呼ばれる。 {(0,4),(2,4),(3,4)} の辺はすべて頂点 4 の入辺である。

 

Figure 2: An example of a directed graph.

 

続く節では、 BGL を用いてこの例のグラフを構築し、様々な方法で操作してみる。 この例の完全なソースコードはexamples/quick_tour.cpp で見ることができる。 各節はこのソースファイルの"一部分"について議論する。 プログラムの出力からの引用も載せている。

 

Constructing a Graph

BGL インタフェースの主な考え方を示すために、この例では BGL の adjacency_list クラスを用いる。 adjacency_list クラスは古典的な " 隣接リスト " データ構造を一般化したものである。 adjacency_list は六つのテンプレートパラメータを持つテンプレートクラスである。 ここで我々は最初の三つのパラメータだけを埋めて、残りの三つについてはデフォルトのものを用いている。 最初の二つのテンプレート引数 (vecS, vecS) は、グラフの各頂点に対する出辺を表現するデータ構造と、 グラフの頂点セットを表現するデータ構造を決定する (各データ構造のトレードオフについての情報はChoosing the Edgelist and VertexListを参照)。 三番目の引数 bidirectionalS は、出辺と入辺の両方へのアクセスを提供する有向グラフを選択している。 第三引数への他のオプションとしては、出辺のみの有向グラフを選択する directedS と、 無向グラフを選択する undirectedS がある。

グラフの型を選択した後、 グラフオブジェクトを宣言し、 MutableGraph インタフェース (adjacency_list が実装している) の add_edge() 関数を用いて辺を埋めることで、 Figure 2 のグラフを作ることができる。 (intの) 組の配列 edge_array は、例の辺を明示的に作成するための便利な方法として用いているだけである。

 

  #include <iostream>                  // for std::cout
  #include <utility>                   // for std::pair
  #include <algorithm>                 // for std::for_each
  #include <boost/graph/graph_traits.hpp>
  #include <boost/graph/adjacency_list.hpp>
  #include <boost/graph/dijkstra_shortest_paths.hpp>

  using namespace boost;
  
  int main(int,char*[])
  {
    // グラフの型のための typedef を作成
    typedef adjacency_list<vecS, vecS, bidirectionalS> Graph;

    // 頂点のため便宜上のラベルを作る
    enum { A, B, C, D, E, N };
    const int num_vertices = N;
    const char* name = "ABCDE";

    // グラフの辺を書き出す
    typedef std::pair<int, int> Edge;
    Edge edge_array[] = 
    { Edge(A,B), Edge(A,D), Edge(C,A), Edge(D,C),
      Edge(C,E), Edge(B,D), Edge(D,E), };
    const int num_edges = sizeof(edge_array)/sizeof(edge_array[0]);

    // グラフオブジェクトを宣言
    Graph g(num_vertices);

    // グラフオブジェクトに辺を追加
    for (int i = 0; i < num_edges; ++i)
      add_edge(edge_array[i].first, edge_array[i].second, g);
    ...
    return 0;
  }

各辺に add_edge() 関数を呼ぶ代わりに、 グラフの 辺イテレータのコンストラクタ を使うことができる。 典型的にはこれは add_edge() を使うより効率がよい。 edge_array へのポインタはイテレータと見なされるため、 配列の先頭と終端へのポインタを渡すことでイテレータのコンストラクタを呼び出すことができる。

    Graph g(edge_array, edge_array + sizeof(edge_array) / sizeof(E), num_vertices);

最初に頂点の個数を明確にしてグラフを作成する代わりに、 MutableGraph インタフェースの add_vertex() 関数と remove_vertex() 関数 で頂点を追加、削除することもできる。

Accessing the Vertex Set

さて、グラフができあがった。グラフのデータにアクセスする為に、このグラフインターフェイスを 様々な方法で用いることができる。 第一に、 VertexListGraph インタフェースの vertices() 関数を用いて グラフのすべての頂点にアクセスできる。 この関数は頂点イテレータstd::pair を返す (一つ目のイテレータは"始めの"頂点を指し、二つ目のイテレータは"最後の次"を指す) 。 頂点イテレータを参照外しすると頂点オブジェクトが得られる。 頂点イテレータの型は graph_traits クラスによって与えられる。 異なるグラフクラスは異なる頂点イテレータ型に関連づけられている点に注意して欲しい。そのために graph_traits が必要とされるのである。 いくつかのグラフ型では、 graph_traits クラスが vertex_iterator 型へのアクセスを提供する。

次の例はグラフの各々の頂点に対するインデックスを表示する。 インデックスを含むすべての頂点と辺のプロパティはプロパティマップオブジェクトを通じて アクセスできる。 property_map クラスは (BGL の定義済みプロパティの一つである vertex_index_t によって特定された) あるプロパティのためのプロパティマップ型を取得するのに使われる。 また get(vertex_index, g) 関数呼び出しは実際のプロパティマップオブジェクトを返す。

 

  // ...
  int main(int,char*[])
  {
    // ...

    // 頂点インデックスのためのプロパティマップを得る
    typedef property_map<Graph, vertex_index_t>::type IndexMap;
    IndexMap index = get(vertex_index, g);

    std::cout << "vertices(g) = ";
    typedef graph_traits<Graph>::vertex_iterator vertex_iter;
    std::pair<vertex_iter, vertex_iter> vp;
    for (vp = vertices(g); vp.first != vp.second; ++vp.first)
      std::cout << index[*vp.first] <<  " ";
    std::cout << std::endl;
    // ...
    return 0;
  }
出力は次の通り:
  vertices(g) = 0 1 2 3 4

 

Accessing the Edge Set

グラフの辺の集合には EdgeListGraph インタフェースの edges() 関数を用いて アクセスすることができる。 vertices() 関数と同様に、 これはイテレータの組を返すが、この場合のイテレータは 辺イテレータ である。 辺イテレータを参照外しすると辺オブジェクトが得られる。 イテレータに明示的に std::pair を作成する代わりに、 今回は tie() ヘルパ関数を用いている。 この便利な関数は std::pair の中身を二つの変数、この場合 eiei_end に 代入する為に使う事ができる。 これはたいていの場合 std::pair を作成するよりずっと便利であり、 BGL の為に我々が選択した方法である。

 

  // ...
  int main(int,char*[])
  {
    // ...
    std::cout << "edges(g) = ";
    graph_traits<Graph>::edge_iterator ei, ei_end;
    for (tie(ei, ei_end) = edges(g); ei != ei_end; ++ei)
        std::cout << "(" << index[source(*ei, g)] 
                  << "," << index[target(*ei, g)] << ") ";
    std::cout << std::endl;
    // ...
    return 0;
  }
出力は以下の通り:
  edges(g) = (0,1) (0,2) (0,3) (0,4) (2,0) (2,4) (3,0)
    (3,1) (3,4) (4,0) (4,1)

 

The Adjacency Structure

続くいくつかの例で、特定の頂点の視点からグラフの隣接構造を探索しよう。 頂点の入辺、出辺、そして近接する頂点を見て回る。 これを "exercise vertex" 関数に閉じこめて、グラフの各頂点に適用してみよう。 BGL と STL との相互運用性を示すために、 STL の for_each() 関数を 使って頂点を辿り、関数を適用してみることにする。

 

  //...
  int main(int,char*[])
  {
    //...
    std::for_each(vertices(g).first, vertices(g).second,
                  exercise_vertex<Graph>(g));
    return 0;
  }

exercise_vertex には、ただの関数の代わりにファンクタを用いる。 なぜなら各頂点の情報にアクセスする際にグラフオブジェクトが必要となるためである; ファンクタを使えば、 std::for_each() が実行されている間、 グラフオブジェクトへの参照を保持する場所が与えられるからだ。 また、ファンクタをグラフの型についてテンプレートにすることにより、異なるグラフクラスで 再利用できるようになる。 ここが exercise_vertex ファンクタの始まりである:

 

  template <class Graph> struct exercise_vertex {
    exercise_vertex(Graph& g_) : g(g_) {}
    //...
    Graph& g;
  };

 

Vertex Descriptors

ファンクタの operator() メソッドを書くために最初に知る必要がある ものは、グラフの頂点オブジェクトの型である。 頂点の型は operator() メソッドへのパラメータとなる。 正確には、実際の頂点オブジェクトではなくむしろ頂点記述子 を扱う。 隣接リストのようなグラフ表現の多くは実際の頂点オブジェクトを保持しないが、 一方で、例えばポインタでリンクされたグラフなどはそうではない。 この違いは頂点記述子という "ブラックボックス" の下に隠される。 頂点記述子は各々のグラフ型によって提供され、 out_edges()in_edges()adjacent_vertices() および 次節で述べるプロパティマップ関数によって、グラフに関する情報へアクセスする為に使われる。 vertex_descriptor 型は graph_traits クラスを通じて取得される。 スコープ :: 演算子の左側 (graph_traits<Graph> 型) が テンプレートパラメータ (Graph 型) に依存しているので、 以下で使われている typename キーワードは必須である。 ファンクタの適用メソッド(apply method)は 次のように定義する:

 

  template <class Graph> struct exercise_vertex {
    //...
    typedef typename graph_traits<Graph>
      ::vertex_descriptor Vertex;

    void operator()(const Vertex& v) const
    {
      //...
    }
    //...
  };

 

Out-Edges, In-Edges, and Edge Descriptors

頂点の出辺は IncidenceGraph インタフェースの out_edges() 関数でアクセスされる。 out_edges() 関数は二つの引数を取る:一つ目の引数は頂点、二つ目はグラフオブジェクトである。 この関数は、頂点のすべての出辺へのアクセスを提供するイテレータの組を返す (vertices() 関数がどのようにイテレータの組を返すのか、と同じように)。 そのイテレータは 出辺イテレータ 呼ばれ、 これらのイテレータの一つを参照外しすることで辺記述子オブジェクトが得られる。 辺記述子は頂点記述子と同種の役割を演じる、グラフ型が提供する "ブラックボックス" である。 以下のコード断片は頂点 v の各出辺に対する始点 - 終点の組を表示するものである。

 

  template <class Graph> struct exercise_vertex {
    //...
    void operator()(const Vertex& v) const
    {
      typedef graph_traits<Graph> GraphTraits;
      typename property_map<Graph, vertex_index_t>::type 
        index = get(vertex_index, g);

      std::cout << "out-edges: ";
      typename GraphTraits::out_edge_iterator out_i, out_end;
      typename GraphTraits::edge_descriptor e;
      for (tie(out_i, out_end) = out_edges(v, g); 
           out_i != out_end; ++out_i) {
        e = *out_i;
        Vertex src = source(e, g), targ = target(e, g);
        std::cout << "(" << index[src] << "," 
                  << index[targ] << ") ";
      }
      std::cout << std::endl;
      //...
    }
    //...
  };
頂点 0 についての出力は以下の通り:
  out-edges: (0,1) (0,2) (0,3) (0,4)

BidirectionalGraph インタフェースの in_edges() 関数は、 入辺イテレータ を通じて頂点のすべての入辺へのアクセスを提供する。 Directed テンプレートパラメータに bidirectionalS が与えらている場合は、 in_edges() 関数は adjacency_list に対してのみ利用できる。 directedS でなく bidirectionalS が定義されている時は、 メモリ容量に余分のコストが掛かる。

 

  template <class Graph> struct exercise_vertex {
    //...
    void operator()(const Vertex& v) const
    {
      //...
      std::cout << "in-edges: ";
      typedef typename graph_traits<Graph> GraphTraits;
      typename GraphTraits::in_edge_iterator in_i, in_end;
      for (tie(in_i, in_end) = in_edges(v,g); 
           in_i != in_end; ++in_i) {
        e = *in_i;
        Vertex src = source(e, g), targ = target(e, g);
        std::cout << "(" << index[src] << "," << index[targ] << ") ";
      }
      std::cout << std::endl;
      //...
    }
    //...
  };
頂点 0 についての出力は以下の通り:
  in-edges: (2,0) (3,0) (4,0)

 

Adjacent Vertices

ある頂点の出辺が与えられたとき、その各辺の終点は始点に隣接しているという。 ときどき、アルゴリズムはグラフの辺を見る必要がなく、頂点にのみ注目することがある。 そのためグラフインタフェースは隣接する頂点への直接アクセスを提供する関数、 AdjacencyGraph インタフェースの adjacent_vertices() 関数を含んでいる。 この関数は隣接イテレータの組を返す。 隣接イテレータを参照外しすると、隣接する頂点の頂点記述子が得られる。

 

  template <class Graph> struct exercise_vertex {
    //...
    void operator()(Vertex v) const
    {
      //...
      std::cout << "adjacent vertices: ";
      typename graph_traits<Graph>::adjacency_iterator ai;
      typename graph_traits<Graph>::adjacency_iterator ai_end;
      for (tie(ai, ai_end) = adjacent_vertices(v, g);
           ai != ai_end; ++ai)
        std::cout << index[*ai] <<  " ";
      std::cout << std::endl;
    }
    //...
  };
頂点 4 についての出力は:
  adjacent vertices: 0 1

 

Adding Some Color to your Graph

BGL は、プロパティをグラフに加えるやり方を工夫することで、 可能な限りフレキシブルであろうと試みている。 例えば、辺の重みのようなプロパティはグラフオブジェクトの生存期間を通じて使われるだろうから、 グラフオブジェクトがプロパティの記憶域も管理してくれるのが便利だろう。 その一方で、頂点の色のようなプロパティは一つのアルゴリズムの間だけ必要とされるので、 グラフオブジェクトとは別に格納する方が良いだろう。 プロパティの種類の一つ目は内部格納プロパティ(internally stored property)と呼ばれ、もう一つは 外部格納プロパティ(externally stored property)と呼ばれる。 グラフアルゴリズム内部にある両方の種類のプロパティにアクセスするために、 BGL はプロパティマップインタフェースと呼ばれる単一の機構を用いる。 プロパティマップについては Property Map Concepts 節で述べる。 加えて、 PropertyGraph コンセプトは 内部格納プロパティのプロパティマップオブジェクトを取得するための インタフェースを定義している。

BGLの adjacency_list クラスは グラフクラスのプラグインテンプレートパラメータを通じて 内部格納プロパティを指定できるようにする。 どのようにこれを行うかは、 Internal Properties 節で詳細に議論されている。 外部格納プロパティは多くの異なる方法で作成することができるが、 最終的にはグラフアルゴリズムに別々の引数として渡される。 プロパティを格納する簡単な方法の一つは、頂点インデックスまたは辺インデックスで インデックスを付けられた配列を作成することである。 VertexList テンプレートパラメータに対して vecS を指定した adjacency_list では、 vertex_index_t のプロパティマップにアクセスされるインデックスが自動的に頂点に割り当てられる。 辺には自動的にインデックスが割り当てられることはないが、プロパティのメカニズムを用いてインデックスを付けることができる。 このインデックスは他の外部格納プロパティをインデックスするのに利用できる。

以下の例で、我々はグラフを構築し、 dijkstra_shortest_paths() を適用する。 この例の完全なソースコードは examples/dijkstra-example.cpp にある。 Dijkstra のアルゴリズムは出発する頂点からグラフの他の任意の頂点までの最短距離を求める。

Dijkstra のアルゴリズムは、各辺に重みプロパティを、 各頂点に距離プロパティを関連づけられていることを要求する。 ここで、重みについては内部プロパティを、距離については外部プロパティを用いることにする。 重みプロパティには property クラスを用いて、重み値を表現する型として int を、 プロパティタグに edge_weight_t (BGL で定義済みのプロパティタグの一つ) を用いる。 重みプロパティは adjacency_list のテンプレート引数として使われる。

listS 型と vecS 型は adjacency_list の内部で使われるデータ構造を決定する選択子である (Choosing the Edgelist and VertexList 節を参照) 。 directedS 型はグラフが有向であることを指定する (でなければ無向である) 。 以下のコードはグラフ型の指定とグラフの初期化を示す。 辺と重みはイテレータ (RandomAccessIterator で修飾された ポインタ) の形でグラフのコンストラクタに渡される。

 

  typedef adjacency_list<listS, vecS, directedS, 
                         no_property, property<edge_weight_t, int> > Graph;
  typedef graph_traits<Graph>::vertex_descriptor Vertex;
  typedef std::pair<int,int> E;

  const int num_nodes = 5;
  E edges[] = { E(0,2), 
                E(1,1), E(1,3), E(1,4),
                E(2,1), E(2,3), 
                E(3,4),
                E(4,0), E(4,1) };
  int weights[] = { 1, 2, 1, 2, 7, 3, 1, 1, 1};

  Graph G(num_nodes, edges, 
          edges + sizeof(edges) / sizeof(E), weights);

外部の距離プロパティについては、記憶域として std::vector を用ることにしよう。 BGL のアルゴリズムはランダムアクセスイテレータをプロパティマップとして処理する。 そのため、単に頂点ベクタの始端イテレータを Dijkstra のアルゴリズムに渡すだけでよい。 上述の例の続きで、以下のコードは距離ベクタを作成し、 Dijkstra のアルゴリズム (暗黙に内部の辺の重みプロパティを用いる)を呼び出し、そして結果の出力を示す。

 

  // 距離プロパティを保持するベクタ
  std::vector<int> d(num_vertices(G));

  // 最初の頂点を得る
  Vertex s = *(vertices(G).first);
  // Dijkstra のアルゴリズムの variant 2 を呼び出す
  dijkstra_shortest_paths(G, s, distance_map(&d[0]));

  std::cout << "distances from start vertex:" << std::endl;
  graph_traits<Graph>::vertex_iterator vi;
  for(vi = vertices(G).first; vi != vertices(G).second; ++vi)
    std::cout << "distance(" << index(*vi) << ") = " 
              << d[*vi] << std::endl;
  std::cout << std::endl;
出力は以下の通り:
  distances from start vertex:
  distance(0) = 0
  distance(1) = 6
  distance(2) = 1
  distance(3) = 4
  distance(4) = 5

 

Extending Algorithms with Visitors

ライブラリのあるアルゴリズムは、あなたの求めるものとほとんど一致するが、 完全に、ではないことがしばしばある。 例えば、前節で我々は各頂点の最短距離を求めるのに Dijkstra のアルゴリズムを用いた。しかしおそらく 最短経路のツリーを記録したいとも思うだろう。 これを行う一つの方法は、最短経路ツリーの各ノードについて先行点 (親) を記録することである。

もし我々が Dijkstra アルゴリズムの書き直しを回避できて、先行点を記録するのに必要な ほんのちょっとしたものを追加するだけでよいとしたら、素晴らしいだろう。 STL では、この類の拡張性は各アルゴリズムの省略可能なパラメータであるファンクタによって提供されている。 BGL では、この役割はビジターによって実現されている。

ビジターはファンクタに似ているが、"適用"メソッドを一つではなくいくつか持っている。 それぞれのメソッドはアルゴリズム内部の特定の明確に定義された時点で呼び出される。 ビジターメソッドは Visitor Concepts 節で詳説されている。 BGL は、先行点を記録するものを含めて、一般的なタスクのためのビジターをいくつか提供している。 ユーザは BGL を拡張する方法として自分独自のビジターを書くことを推奨される。 ここでは先行点を記録するビジターの実装と使い方を見ていこう。 dijkstra_shortest_paths() アルゴリズムを使うのだから、 我々が作るビジターは Dijkstra Visitor でなければならない。

record_predecessors ビジターの機能は二つの部分に分けられる。 先行点プロパティを格納しアクセスするために プロパティマップを用いる。 先行点ビジターは、その後、どの親の記録するかにのみ責任を持つ。 これを実装するために、 record_predecessors クラスを作成し、それを 先行点プロパティマップ PredecessorMap についてテンプレート化する。 このビジターはビジターメソッドの一つを埋めているだけなので、 残りのために空のメソッドを提供してくれる dijkstra_visitor を継承する。 predecessor_recorderのコンストラクタはプロパティマップオブジェクトを 受け取り、データメンバの中に保存する。

 

  template <class PredecessorMap>
  class record_predecessors : public dijkstra_visitor<>
  {
  public:
    record_predecessors(PredecessorMap p)
      : m_predecessor(p) { }

    template <class Edge, class Graph>
    void edge_relaxed(Edge e, Graph& g) {
      // target(e) の親に source(e) をセット
      put(m_predecessor, target(e, g), source(e, g));
    }
  protected:
    PredecessorMap m_predecessor;
  };

先行点を記録する作業は非常に簡単である。 Dijkstra のアルゴリズムが辺に対して緩和操作を施したとき (それを最短経路木に追加することもある) 、 その始点を終点の先行点として記録する。 その後、もし辺が再び緩和されたら、先行点プロパティは新しい先行点によって上書きされる。 ここで先行点を記録するためにプロパティマップに関連づけられた put() 関数を用いる。 ビジターの edge_filter は、 explore() メソッドをいつ呼び出すかをアルゴリズムに伝える。 この場合、最短経路ツリーの辺に関して通知したいだけなので、 tree_edge_tag を指定する。

最後に(as a finishing touch)、先行点ビジターの作成をより便利にするヘルパー関数を作成する。 すべての BGL ビジターはこのようなヘルパー関数を持っている。

 

  template <class PredecessorMap>
  record_predecessors<PredecessorMap>
  make_predecessor_recorder(PredecessorMap p) {
    return record_predecessors<PredecessorMap>(p);
  }

さて、 record_predecessors を Dijkstra のアルゴリズムで使う準備が整った。 幸運なことに、 BGL の Dijkstra アルゴリズムは既にビジターを扱う機能を備えているので、 我々の新しいビジターを渡すだけである。 この例では一つのビジターを用いるだけでよいが、 BGL は一つのアルゴリズムで複数のビジターを 用いる方法も用意している (Visitor Concepts 節を参照) 。

 

  using std::vector;
  using std::cout;
  using std::endl;
  vector<Vertex> p(num_vertices(G)); //先行点の配列
  dijkstra_shortest_paths(G, s, distance_map(&d[0]). 
                          visitor(make_predecessor_recorder(&p[0])));

  cout << "parents in the tree of shortest paths:" << endl;
  for(vi = vertices(G).first; vi != vertices(G).second; ++vi) {
    cout << "parent(" << *vi;
    if (p[*vi] == Vertex())
      cout << ") = no parent" << endl; 
    else 
      cout << ") = " << p[*vi] << endl;
  }
出力は以下の通り:
  parents in the tree of shortest paths:
  parent(0) = no parent
  parent(1) = 4
  parent(2) = 0
  parent(3) = 2
  parent(4) = 3

Notes

[1] Dijkstra アルゴリズムの新しいバージョンでは 先行点を記録するための名前付きパラメータが含まれている。 そのため先行点ビジターはもはや必要ではないが、今でも優れた例として役に立っている。

Copyright © 2000 Jeremy Siek, Indiana University (jsiek@osl.iu.edu)

Japanese Translation Copyright © 2003 Kent.N
オリジナルの、及びこの著作権表示が全ての複製の中に現れる限り、この文書の複製、利用、変更、販売そして配布を認める。このドキュメントは「あるがまま」に提供されており、いかなる明示的、暗黙的保証も行わない。また、いかなる目的に対しても、その利用が適していることを関知しない。

このドキュメントの対象: Boost Version 1.29.0
最新版ドキュメント (英語)