はじめに
今回から数回に渡って、離散数学の中心的な分野である「グラフ理論」について、個人的にまとめていこうと思う。
なお、当ブログでは競技プログラミングの話題が中心ということもあり、理論について詳しく扱うというよりはむしろ、プログラミングにおけるグラフ理論の扱い (各アルゴリズムの C++ による実装など) を中心に書いていこうと考えている。投稿ペースは不定期になると予想されるので悪しからず。
さて、初回となる今回は、「グラフ理論で扱う『グラフ』とは何か」「グラフ理論の基本用語」などを中心に扱う。この回はかなり辞書的な内容になると思うので、退屈に感じたらとりあえず先へ進んで必要に応じて戻ってくる、という流れでも構わない。
「グラフ」とは何か?
高校数学までは、「グラフ」といったら関数を座標平面(空間)上にプロットしたものだったり、たくさんのデータを棒状や円状にまとめたものだったりを指す用語であった。
しかし、グラフ理論における「グラフ」というのは「対象物の関係性を表すもの」である。通常、対象物は丸 (頂点) で表され、対象物間の関係は線あるいは矢印 (辺) で表される。
身近な例で言えば、 SNS 上のアカウントの相互関係や、鉄道路線図が分かりやすいだろう。
さて、このような丸と丸を線でつないだ関係について考えることには、どのような意味があるのだろうか。
先ほどの例で言えば、 SNS 上のアカウントの相互関係をグラフとしてモデル化して分析することで、どのようなコミュニティが存在するのかや情報がどのように拡散されていくのかといったことが分かる。
また、駅と駅がどのような路線で結ばれているのか、また各駅間の所要時間・運賃をモデル化することで、ある駅からある駅までの最短経路や最安経路を求めることもできるのだ。
このように、世の中の問題をグラフに関する問題として捉えることで、見通しよく考えることができるようになることがある。グラフ理論を学ぶ意義はこのようなところにあると私は思っている。
グラフの定義
それでは、改めて「グラフ」を数学的に定義してみる。
また、頂点集合の要素数 (位数) を 、辺集合の要素数を と表す。
グラフ理論の基本用語
続いて、グラフ理論に登場する基本的な用語について紹介する。
頂点の接続
頂点 が辺 によって結ばれているとき、「辺 は頂点 と頂点 に接続している」という。また、このときの を の端点という。
頂点の接続には単純な接続の他にも「多重辺」「自己ループ」といったパターンがある。
また、多重辺や自己ループを持つグラフを「多重グラフ」、そうでないグラフを「単純グラフ」という。
辺の重み
各辺に実数値をとる「重み (weight) 」が付随したグラフを考えることもある。このようなグラフを「重み付きグラフ」という。逆にそうでないことを強調するときは「重みなしグラフ」という。
辺に重みを加えることで、最短経路や最小コストの経路を求めたいときに有効になる。
有向グラフと無向グラフ
各辺に「向き」があるグラフを考えることもある。向きがある辺 (有向辺) を1つ以上含むグラフを「有向グラフ」といい、有向辺については矢印で表す。逆にそうでないグラフを「無向グラフ」という。
有向辺は2つの頂点の順序対で表され、 のように書く。 であることに注意。
以降、単に「グラフ」と書くときは単純無向グラフを指す。
有向グラフは一方通行の経路を示すときに有効である。
頂点の隣接と次数
頂点 が辺 によって接続されているとき、「頂点 は隣接している」という。また、別の辺 が存在するとき (2つの辺が1つの端点を共有しているとき) 、「辺 は隣接している」という。
ある頂点について、その頂点に接続している辺の数 (隣接している頂点の数) をその頂点の次数 (degree) といい、グラフ の頂点 の次数を と書く。
また、多重辺の場合は辺の数だけ次数もカウントされ、自己ループの場合は次数を2回カウントする。
上のグラフ では、
となる。
なお有向グラフでは、ある頂点に向かってくる辺の数を入次数、ある頂点から出ていく辺の数を出次数といい、それぞれ と書くことがある。
上のグラフ では、例えば
となる。
部分グラフ・誘導部分グラフ
あるグラフの頂点や辺の一部を取り出してきたグラフを部分グラフという。数学的に定義すると次のようになる。
また、あるグラフの頂点の一部を取り出し、それらの頂点に接続する辺は元のグラフと同じであるグラフを誘導部分グラフという。数学的な定義は次の通り。
ウォーク・トレイル・サーキット・パス
あるグラフ の部分グラフの一種として他に重要なものには、以下が挙げられる。
- ウォーク (walk) : 上の任意の頂点 について、 から へ隣接する頂点をたどりながら到達することができるとき、その経路を「 ウォーク」や「 から への歩道」などといい、このときの を始点、 を終点という。同じ頂点や辺を通ることも許容される。
- トレイル (trail) : ウォーク に同じ辺が含まれていないとき、その経路をトレイル (小道) という。同じ頂点を通ることは許容される。
- サーキット (circuit) : トレイル の始点と終点が同じであるとき、その経路をサーキット (回路) という。つまり、同じ辺を2回以上通らずに初めの頂点に戻ってくるような頂点の辿り方のことである。
- パス (path) : ウォーク に同じ頂点が含まれていないとき、その経路をパス (道) という。始点と終点が同じものを特に閉路 (cycle) という。つまり、同じ頂点を2回以上通らずに初めの頂点に戻ってくるような頂点の辿り方のことである。
ただし、これらの語の定義については参考書間で微妙に異なっていることがあるので注意してほしい。このブログでは上の定義に従って書くことにする。
下のグラフを例にとると、
- ウォーク :
- トレイル :
- サイクル :
- パス :
※ここでは分かりやすいよう矢印で繋いでいるが、実際に表記するときはカンマで列挙することが多い。
また、有向グラフ上でもこれらの語は同様に定義され、有向であることを強調するために「有向○○」ということがある。
さらに、これらの語の「長さ」といったときには、重みなしグラフでは単にその経路上の辺の本数を、重みありグラフでは経路上の重みの総和を表す。
グラフの連結性
無向グラフ の任意の2頂点 に対して パスが存在するとき、 は連結 (連結グラフ) であるという。連結グラフでないグラフは非連結グラフといい、これはいくつかの連結部分グラフに分割することができる。これらを連結成分という。
上のグラフは非連結グラフだが、各連結部分グラフ (3つの頂点集合 からなる3つの部分グラフ) は元のグラフの連結成分に当たる。
有向グラフについても連結性を定義できるが、辺の向きが存在することに注意する必要がある。
任意の2頂点 に対して パスと パスがともに存在するとき強連結であるといい、有向グラフを無向グラフとして捉えたときに連結となるとき弱連結であるという。
上の図で、左のグラフでは は到達可能だが は到達不可能である。したがってこのグラフは強連結ではない (弱連結ではある) 。
一方、右の2つのグラフは左のグラフの部分グラフである。これらのグラフはそれぞれ任意の2頂点間を互いに行き来できるので強連結である。このような部分グラフを、元のグラフの強連結成分という。
握手補題
すべてのグラフにおいて、辺の本数と次数との間にはとある関係が成り立つことが知られている。これを握手補題という。
端的に言ってしまえば、「グラフ上の各頂点の次数の総和は、辺の本数の2倍に等しい」ということである。これは、グラフ上の次数の総和は偶数であるということも示している。
証明は意外と簡単で、数学的帰納法による。
【証明】
グラフ について、 とする。
のとき
頂点の次数の総和は であり、 であるから成立。
のとき
握手補題の成立を仮定すると、 のとき (辺を1本追加したとき) 、追加した辺の両端点の次数が1ずつ増加するので、次数の総和は 。一方、辺の本数は 。したがって、このときも成立。
より、数学的帰納法により握手補題が成り立つことが示された。 (証明終)
この補題はグラフ理論における基本定理であり、詳しくは書かないがこれを利用することで他のグラフに関する定理の証明をすることもできる。
グラフを表すデータ構造
さて、ここまで様々なグラフについて見てきたが、これをコンピュータ上で表現するにはどうすればよいのだろう。
コンピュータ上でグラフを表す際に用いられるデータ構造として代表的なのは、隣接行列と隣接リストである。それぞれ簡単に解説する。
隣接行列
※この項では行列の知識を少し必要とする。
隣接行列は隣接している頂点同士の関係を表した行列であり、型は の正方行列となる。各成分の定義は次の通り。
上の2つのグラフを隣接行列で表現すると、以下のようになる。
見てわかる通り、無向グラフの隣接行列は常に対称行列となる。
このデータ構造は、 C++ では二次元配列を用いて実現することができる。
無向グラフ について、 と各 が標準入力によって次のように与えられたとする。
N M u_1 v_1 u_2 v_2 ... u_M v_M
このとき、 の隣接行列の各成分を標準出力するコードは次のようになる。
※本来は固定長の配列でも良いが、説明の都合上可変長配列vector
を使っている。
#include <iostream> #include <vector> int main() { int N, M; std::cin >> N >> M; std::vector<std::vector<int>> graph(N, std::vector<int>(N, 0)); // 隣接行列 for (int i = 0; i < M; i++) { // 入力は 1-indexed int u, v; std::cin >> u >> v; graph[u - 1][v - 1] = 1; graph[v - 1][u - 1] = 1; // 有向グラフの場合は不要 } for (auto &&row : graph) { for (auto &&x : row) { std::cout << x << " "; } std::cout << std::endl; } return 0; }
例えば上図の左のグラフを表す入力 :
3 3 1 2 2 3 3 1
を与えると、出力は
0 1 1 1 0 1 1 1 0
となり、先ほどの数式による表現と一致した。
隣接行列は辺の追加・削除や存在確認が高速に行える一方、 のメモリを消費してしまうので、グラフに対する辺の数が比較的多いグラフ (密グラフ) の場合に有効となるデータ構造である。なお、辺の重みを格納できないため有向グラフに対しては使えない。
隣接リスト
隣接リストは、グラフ の各頂点 に対して辺 が存在するような頂点 を列挙する表現方法である。つまり、隣接行列の各行に対して となるような を取り出してきたものだ。
先ほどの2つのグラフ (下図に再掲) を隣接リストで表現すると、以下のようになる。
C++ では、可変長配列vector
の2次元配列を用いれば実現できる。上述の標準入力から隣接リストを構築し、その内容を出力するコードは以下のようになる。
#include <iostream> #include <vector> int main() { int N, M; std::cin >> N >> M; std::vector<std::vector<int>> graph(N); // 隣接リスト for (int i = 0; i < M; i++) { // 入力は 1-indexed int u, v; std::cin >> u >> v; graph[u - 1].push_back(v - 1); graph[v - 1].push_back(u - 1); // 有向グラフの場合は不要 } for (auto &&v : graph) { for (auto &&x : v) { std::cout << x + 1 << " "; // 出力は 1-indexed } std::cout << std::endl; } return 0; }
入力に対する出力は
2 3 1 3 2 1
となる。
隣接リストはメモリ消費が であるため広く用いられるデータ構造である。グラフに対する辺の数が比較的少ないグラフ (疎グラフ) の場合に特に有効である。
おわりに
いかがだっただろうか。今回は、離散数学の中心となる分野であるグラフ理論について、そもそも「グラフ」とは何かや、グラフ理論に登場する基本用語を紹介した。
一口にグラフと言っても様々な切り口から見たグラフの分類が存在するので、特徴を押さえながら理解していこう。
次回はグラフの特殊な形である「木」についてまとめていこうと思う。