C++ Boost

Review of Elementary Graph Theory

この章は、基本的なグラフ理論を思い出させることを意図している。 読者があらかじめグラフアルゴリズムの知識があるのなら、始めるにあたりこの章は十分であろう。 もし読者がグラフアルゴリズムの知識がないのならば、 Cormen, Leiserson, RivestのIntroduction to Algorithms のようなもっと詳しいものを薦める。

The Graph Abstraction

グラフは、多くの種類の問題を解くのに有効な数学的抽象化である。 基本的には、グラフは頂点と辺から構成され、辺は二つの頂点を結ぶ。 もっと正確には、 グラフ(graph)とは組(V,E)で表され、 Vは有限集合で、EVの2項関係である。 V頂点集合(vertex set) と呼ばれ、その要素を 頂点(vertex) と呼ぶ。 Eは辺の集合で、 辺(edge) とは(u,v)の組でu,vVの要素である。 有向グラフ(directed graph)においては、 辺は順序付けられた組で、 始点(source)終点(target)へと接続する。 無向グラフ(undirected graph)においては、 辺は順序付けされていない組で、2つの頂点を両方向につなぐ。 つまり、無向グラフでは (u,v)(v,u)は同じ辺の2通りの書き方である。

グラフのこの定義はいくつかの点であいまいである。 辺や頂点が何を表現するかが述べられていない。 グラフの例としては、連絡道路やハイパーリンク付きのウェブページなどを挙げることができる。 これらの詳細がグラフの定義からは除外されているのは、大きな理由がある。 それらの詳細はグラフの抽象化の中では必要な部分ではない。 詳細を定義から除外することで再利用可能な理論を構築でき、 そのことは多くの異なった種類の問題を解く際に役に立つのである。

定義にもどろう。グラフは頂点と辺の集合である。 実際の様子を見せるため、頂点に文字のラベルがついたグラフを考え、辺を単純に文字の組としよう。 ここで、有向グラフの例を次のように書くことができる。


V = {v, b, x, z, a, y }
E = { (b,y), (b,y), (y,v), (z,a), (x,x), (b,x), (x,v), (a,z) }
G = (V, E)

このグラフを図示すると 図1 のようになる。 辺 (x,x)輪(self-loop)と呼ばれる。 (b,y)(b,y)平行辺(parallel edges)であり、これは マルチグラフ(multigraph) でのみ許される(ただし、通常は有向グラフでも無向グラフでも許されない)。

図1: 有向グラフの例

次に似たようなグラフを示すが、今度は無向グラフである。 これは図2に図示する。 無向グラフでは輪は許されない。 上記のグラフ(から平行辺(b,y)を除いたもの)の 無向版(undirected version)である。 それはつまり、同じ頂点をもち、同じ辺から方向を除いたものを持つことを意味し、 (a,z)(z,a)いう2つの辺は一つの辺に退化する。 また、逆を考えることもできる。 無向グラフの有向版(directed version)は、 すべての辺をそれぞれの方向を向く2つの辺で置き換えることで得られる。


V = {v, b, x, z, a, y }
E = { (b,y), (y,v), (z,a), (b,x), (x,v) }
G = (V, E)

図2: 無向グラフの例

ここでさらにグラフの用語を定義する。 辺(u,v)がグラフに含まれるとき、 頂点vは頂点uについて隣接している(adjacent)と言う。 有向グラフでは、辺(u,v)は 頂点u出辺(out-edge)であり、 頂点v入辺(in-edge)である。 無向グラフでは、辺(u,v)は 頂点uv接合している(incident on)という。

図1で、 頂点yは頂点bに対して隣接している (ただしbyに対して隣接していない)。 辺(b,y)bの出辺であり、yの入辺である。 図2で、 ybに隣接していて、また逆も同様である。 辺(y,b)は頂点ybを接合している。

有向グラフにおいて、ある頂点の出辺の数は 出次数(out-degree)と呼ばれ、 入辺の数は 入次数(in-degree)と呼ばれる。 無向グラフにおいて、ある頂点に対して接合している辺の数は 次数(degree)と呼ばれる。 図1で、頂点bの出次数は3であり、 入次数は0である。 図2では単純に頂点bの次数は2である。

グラフの路(path)とは辺の列で、 それぞれの辺の終点が次の辺の始点であるものである。 頂点uから始まり頂点vで終わる路があれば、 頂点vuから到達可能(reachable)であるという。 路が単純(simple)であるとは、 辺の列の中でどの頂点も繰り返し現れないことである。 路<(b,x), (x,v)>は単純であるが、 路<(a,z), (z,a)>は単純ではない。 また、路<(a,z), (z,a)>は最初の頂点と最後の頂点が一致するので、 サイクル(cycle)と呼ばれる。 サイクルのないグラフは アサイクリック(acyclic)と呼ばれる。

planar graph とは、すべての辺が交差しないように平面上に描けるグラフのことである。 そのように描かれたものはplane graphと呼ばれる。 plane graphの面(face)とは、辺に囲まれた連結成分のことである。 planar graphの重要な特性は、 面、辺、頂点の数がオイラーの定理:|F| - |E| + |V| = 2によって関係付けられることである。 このことは、planar graphは最大でもO(|V|)個の辺しか持たないことを意味する。

Graph Data Structures

データ構造を考えるときに最初に考えるべきグラフの属性は、まばらさ(sparsity)である。 まばらさとは頂点に対する相対的な辺の数である。 EがV2に近いグラフは密(dense)であると呼ばれ、 E = alpha ValphaVより十分に小さい場合はまばらな(sparse)グラフと呼ばれる。 密なグラフについては、通常隣接行列表現(adjacency-matrix representation)が最良の選択であり、 一方まばらなグラフについては隣接リスト表現(adjacency-list representation)が最良である。 また、まばらなグラフについては辺リスト表現(edge-list representation)も適切な状況下では記憶効率面でよい選択である。

Adjacency Matrix Representation

グラフの隣接行列表現はV x Vの2次元配列である。 行列auvの要素は、辺(u,v)がグラフに含まれるかどうかを示すブーリアン値である。 図3図1(から(b,y)を引いたもの)の隣接行列表現を表す。 保存に必要な領域はO(V2)である。 任意の辺について、アクセス、追加、除去にかかる時間はO(1)である。 頂点の追加や除去は、再割り当てとすべてのグラフのコピーが必要になり、手順数はO(V2)になる。 adjacency_matrixクラスは、 隣接行列表現によってBGLグラフインターフェースを実装する。

図3: 隣接行列によるグラフの表現

Adjacency List Representation

グラフの隣接リスト表現では、すべての頂点に対して出辺の列を保存する。 まばらなグラフでは、こうすることでメモリ領域を節約でき、必要な領域はO(V + E)だけになる。 さらに、すべての頂点の出辺にはより効果的にアクセスできる。 辺の挿入のコストはO(1)で、任意の辺へのアクセスはO(alpha)である。 ここで、alphaは行列のまばらさ(グラフ中のすべての頂点についての出辺の数の最大値)である。 図4図1のグラフの隣接リスト表現である。 adjacency_listは隣接リスト表現の実装である。

図4: 隣接リストによるグラフ表現

Edge List Representation

グラフの辺リスト表現は、単純に辺の列であり、辺は頂点のIDの組で表される。 必要なメモリはO(E)だけである。 辺挿入のコストはO(1)であり、特定の辺のアクセスするのはO(E)(あまり効果的でない)である。 図5図1のグラフの辺リスト表現である。 edge_listアダプタクラスは、辺リスト表現の実装を作るのに使うことができる。

図5: 辺リストによるグラフの表現

Graph Algorithms

Graph Search Algorithms

木辺(tree edge)とは、グラフ探索アルゴリズムをグラフに適用することによって作られた探索木(またはフォレスト)の辺ことである。 辺(u,v)は木辺であるのは、辺(u,v)の探索(ビジタexplore()メソッドにあたる)をしているときにvが最初に見つかるときである。 後退辺(back edge)とは探索木上で頂点を先祖につなぐ辺である。 したがって、辺(u,v)ではvuの先祖である。 輪は後退辺とみなされる。 先行辺(forward edge)は木辺ではない辺(u,v)で、探索木上uを子孫vへとつなぐ。 交差辺(cross edge)とは、以上の3つのカテゴリに含まれない辺のことである。

Breadth-First Search

広さ優先探索(Breadth-First Search, BFS)とは、グラフに対して横断的であり、特定の原点から到達可能な頂点をすべて探索する。 また横断する順番については、頂点のすべての近傍を探索してから近傍の近傍の探索へと進む。 広さ優先探索について考えるには、例えば水溜りに石を落としたときに波が放射状に広がるように拡散すると思えばよい。 同じ「波」の中の頂点は原点から同じ距離にある。 頂点は最初にアルゴリズムによって遭遇するときに発見される(discovered)と言う。 頂点は、その近傍がすべて探索されたときに完了した(finished)と言われる。 これらをわかりやすくする例がある。 グラフを図6に示し、そのBFSにおける発見と完了の順番をその下に示す。

図6: 広さ優先探索がグラフに広がる様子

  発見の順番: s r w v t x u y 
  完了の順番: s r w v t x u y

sから開始して、最初はrw(sの近傍)にたどり着く。 sの両方の希望に到達してから、 rの近傍(頂点v)に到達し、wの近傍txに到達する (rwの順序は意味を持たない)。 最後にtxの近傍、uyに到達する。

今グラフ上のどこにいるか、次にどこの頂点に行くかをアルゴリズムが把握するために、 BFSは頂点に色を塗る。塗る色を置く場所は、グラフの中でもよいし、アルゴリズムに引数として渡すこともできる。

Depth-First Search

A depth first search (DFS) visits all the vertices in a graph. When choosing which edge to explore next, this algorithm always chooses to go ``deeper'' into the graph. That is, it will pick the next adjacent unvisited vertex until reaching a vertex that has no unvisited adjacent vertices. The algorithm will then backtrack to the previous vertex and continue along any as-yet unexplored edges from that vertex. After DFS has visited all the reachable vertices from a particular source vertex, it chooses one of the remaining undiscovered vertices and continues the search. This process creates a set of depth-first trees which together form the depth-first forest. A depth-first search categorizes the edges in the graph into three categories: tree-edges, back-edges, and forward or cross-edges (it does not specify which). There are typically many valid depth-first forests for a given graph, and therefore many different (and equally valid) ways to categorize the edges.

One interesting property of depth-first search is that the discover and finish times for each vertex form a parenthesis structure. If we use an open-parenthesis when a vertex is discovered, and a close-parenthesis when a vertex is finished, then the result is a properly nested set of parenthesis. Figure 7 shows DFS applied to an undirected graph, with the edges labeled in the order they were explored. Below we list the vertices of the graph ordered by discover and finish time, as well as show the parenthesis structure. DFS is used as the kernel for several other graph algorithms, including topological sort and two of the connected component algorithms. It can also be used to detect cycles (see the Cylic Dependencies section of the File Dependency Example).

Figure 7: Depth-first search on an undirected graph.

  order of discovery: a b e d c f g h i
  order of finish: d f c e b a
  parenthesis: (a (b (e (d d) (c (f f) c) e) b) a) (g (h (i i) h) g)

Minimum Spanning Tree Problem

The minimum-spanning-tree problem is defined as follows: find an acyclic subset T of E that connects all of the vertices in the graph and whose total weight is minimized, where the total weight is given by

w(T) = sum of w(u,v) over all (u,v) in T, where w(u,v) is the weight on the edge (u,v)

T is called the spanning tree.

Shortest-Paths Algorithms

One of the classic problems in graph theory is to find the shortest path between two vertices in a graph. Formally, a path is a sequence of vertices <v0,v1,...,vk> in a graph G = (V, E) such that each vertex is connected to the next vertex in the sequence (the edges (vi,vi+1) for i=0,1,...,k-1 are in the edge set E). In the shortest-path problem, each edge is given a real-valued weight. We can therefore talk about the weight of a path

w(p) = sum from i=1..k of w(vi-1,vi)

The shortest path weight from vertex u to v is then

delta (u,v) = min { w(p) : u --> v } if there is a path from u to v
delta (u,v) = infinity otherwise.

A shortest path is any path who's path weight is equal to the shortest path weight.

There are several variants of the shortest path problem. Above we defined the single-pair problem, but there is also the single-source problem (all shortest paths from one vertex to every other vertex in the graph), the equivalent single-destination problem, and the all-pairs problem. It turns out that there are no algorithms for solving the single-pair problem that are asymptotically faster than algorithms that solve the single-source problem.

A shortest-paths tree rooted at vertex in graph G=(V,E) is a directed subgraph where V' is a subset of V and E' is a subset of E, V' is the set of vertices reachable from , G' forms a rooted tree with root , and for all v in V' the unique simple path from to v in G' is a shortest path from to v in . The result of a single-source algorithm is a shortest-paths tree.

Network Flow Algorithms

A flow network is a directed graph G=(V,E) with a source vertex s and a sink vertex t. Each edge has a positive real valued capacity function c and there is a flow function f defined over every vertex pair. The flow function must satisfy three contraints:

f(u,v) <= c(u,v) for all (u,v) in V x V (Capacity constraint)
f(u,v) = - f(v,u) for all (u,v) in V x V (Skew symmetry)
sumv in V f(u,v) = 0 for all u in V - {s,t} (Flow conservation)

The flow of the network is the net flow entering the sink vertex t (which is equal to the net flow leaving the source vertex s).

|f| = sumu in V f(u,t) = sumv in V f(s,v)

The residual capacity of an edge is r(u,v) = c(u,v) - f(u,v). The edges with r(u,v) > 0 are residual edges Ef which induce the residual graph Gf = (V, Ef). An edge with r(u,v) = 0 is saturated.

The maximum flow problem is to determine the maximum possible value for |f| and the corresponding flow values for every vertex pair in the graph.

A flow network is shown in Figure 8. Vertex A is the source vertex and H is the target vertex.

Figure 8: A Maximum Flow Network.
Edges are labeled with the flow and capacity values.

There is a long history of algorithms for solving the maximum flow problem, with the first algorithm due to Ford and Fulkerson. The best general purpose algorithm to date is the push-relabel algorithm of Goldberg which is based on the notion of a preflow introduced by Karzanov.


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