グラフ理論の基本——頂点と辺が作る関係の数学
グラフとは
「電車の路線図を思い浮かべてください」——駅(点)と路線(線)で表された地図がグラフです。SNSの人間関係図、インターネットのルーター間の接続、道路網——これらすべてが「グラフ」という一つの数学的枠組みで表現できます。
グラフ(graph)とは、頂点(vertex / node)の集合 と、頂点の対を結ぶ辺(edge)の集合 からなる構造 のことです。
グラフは「関係」を表す最も汎用的な数学的構造です——「どの点とどの点がつながっているか」だけを抽象化したもの。
グラフの種類
「グラフにはいくつかの種類がある」——辺の性質や構造によって使い分けます:
| 種類 | 説明 |
|---|---|
| 無向グラフ | 辺に向きがない(友達関係) |
| 有向グラフ(ダイグラフ) | 辺に向きがある(フォロー関係) |
| 重み付きグラフ | 辺に数値(距離・コスト)がある |
| 単純グラフ | 多重辺・自己ループなし |
| 完全グラフ | 全頂点間に辺がある |
| 二部グラフ | 頂点を2組に分け、辺は組をまたぐ |
| 木 | 閉路のない連結グラフ |
基本概念
次数(degree)
「その頂点にいくつの辺がつながっているか」——頂点 の次数 は に接続する辺の数です。
握手補題:「全員で握手すると、握手された手の総数は握手の回数の2倍になる」——同じことが辺と次数にも言えます:
理由:各辺は つの頂点の次数に ずつ貢献するため——「1本の辺は両端の頂点にそれぞれ1を加える」。
系:次数が奇数の頂点の個数は必ず偶数——「握手する手の総数は必ず偶数なので、奇数回握手した人の数も必ず偶数」。
経路・道・閉路
- 歩道(walk):辺をたどる列(繰り返しあり)
- 道(trail):辺を繰り返さない歩道——「同じ道を2度通らない」
- 路(path):頂点も辺も繰り返さない歩道——「同じ場所を2度訪れない」
- 閉路(cycle):起点に戻る路——「出発点に戻ってくるルート」
深さ優先探索(DFS)アニメーション
var nodes, edges, visited, stack, dfsOrder, step, timer;
nodes = [
{ x: 300, y: 60, label: 'A' },
{ x: 160, y: 160, label: 'B' },
{ x: 440, y: 160, label: 'C' },
{ x: 80, y: 270, label: 'D' },
{ x: 240, y: 270, label: 'E' },
{ x: 360, y: 270, label: 'F' },
{ x: 520, y: 270, label: 'G' }
];
edges = [
[0,1],[0,2],[1,3],[1,4],[2,5],[2,6],[3,4],[5,6]
];
function buildAdj() {
var adj = [];
for (var i = 0; i < nodes.length; i++) adj.push([]);
for (var e = 0; e < edges.length; e++) {
adj[edges[e][0]].push(edges[e][1]);
adj[edges[e][1]].push(edges[e][0]);
}
return adj;
}
var adj = buildAdj();
visited = [];
dfsOrder = [];
timer = 0;
step = 0;
// precompute DFS order
function dfs(start) {
var stk = [start], vis = [];
for (var i = 0; i < nodes.length; i++) vis.push(false);
var order = [];
while (stk.length > 0) {
var v = stk.pop();
if (vis[v]) continue;
vis[v] = true;
order.push(v);
var nbrs = adj[v].slice().reverse();
for (var ni = 0; ni < nbrs.length; ni++) {
if (!vis[nbrs[ni]]) stk.push(nbrs[ni]);
}
}
return order;
}
dfsOrder = dfs(0);
function loop() {
ctx.clearRect(0, 0, W, H);
timer++;
if (timer % 40 === 0) {
step = (step + 1) % (dfsOrder.length + 3);
if (step === 0) { /* restart */ }
}
var currentVisited = step;
var visitedSet = {};
for (var vi = 0; vi < Math.min(currentVisited, dfsOrder.length); vi++) {
visitedSet[dfsOrder[vi]] = vi + 1;
}
// draw edges
for (var ei = 0; ei < edges.length; ei++) {
var u = edges[ei][0], v = edges[ei][1];
var bothVisited = visitedSet[u] !== undefined && visitedSet[v] !== undefined;
ctx.strokeStyle = bothVisited ? '#22c55e55' : '#334155';
ctx.lineWidth = bothVisited ? 2.5 : 1.5;
ctx.beginPath();
ctx.moveTo(nodes[u].x, nodes[u].y);
ctx.lineTo(nodes[v].x, nodes[v].y);
ctx.stroke();
}
// draw nodes
for (var ni = 0; ni < nodes.length; ni++) {
var nd = nodes[ni];
var vOrder = visitedSet[ni];
var isCurrent = (currentVisited <= dfsOrder.length && dfsOrder[currentVisited-1] === ni);
if (isCurrent) {
ctx.fillStyle = '#fbbf24';
} else if (vOrder !== undefined) {
var t2 = (vOrder - 1) / (nodes.length - 1);
var r2 = Math.round(34 + t2 * 120);
var g2 = Math.round(211 - t2 * 80);
var b2 = Math.round(99 - t2 * 40);
ctx.fillStyle = 'rgb(' + r2 + ',' + g2 + ',' + b2 + ')';
} else {
ctx.fillStyle = '#1e293b';
}
ctx.beginPath();
ctx.arc(nd.x, nd.y, 22, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = vOrder !== undefined ? '#22c55e' : '#475569';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 15px monospace';
ctx.textAlign = 'center';
ctx.fillText(nd.label, nd.x, nd.y + 5);
if (vOrder !== undefined) {
ctx.fillStyle = '#fbbf24';
ctx.font = '10px monospace';
ctx.fillText(vOrder, nd.x + 16, nd.y - 14);
}
}
// legend
ctx.fillStyle = '#e2e8f0';
ctx.font = '13px monospace';
ctx.textAlign = 'left';
ctx.fillText('DFS 訪問順: ' + dfsOrder.slice(0, currentVisited).map(function(i){return nodes[i].label}).join(' → '), 14, 328);
ctx.fillStyle = '#fbbf24';
ctx.fillText('■ 現在', 14, 346);
ctx.fillStyle = '#22c55e';
ctx.fillText('■ 訪問済み', 80, 346);
ctx.fillStyle = '#475569';
ctx.fillText('■ 未訪問', 180, 346);
requestAnimationFrame(loop);
}
loop(); 探索アルゴリズム
深さ優先探索(DFS)
「迷路を解くときに壁に沿って進む」——スタック(または再帰)を使って、できるだけ深く進んでから戻ります:
DFS(G, s):
visited = {}
stack = [s]
while stack not empty:
v = stack.pop()
if v not in visited:
visited.add(v)
for each neighbor u of v:
if u not in visited:
stack.push(u)
計算量:——「全頂点と全辺を一度ずつ見るだけで済む」
幅優先探索(BFS)
「池に石を投げたときの波紋が広がるように」——キューを使って、近い頂点から順に探索します。最短路(辺の重みが等しい場合)を求められます。
計算量:
オイラー路とオイラー閉路
「すべての橋をちょうど一度ずつ渡って散歩できるか?」——18世紀のケーニヒスベルクという街には川に架かる7本の橋があり、これが「全部の橋を1度ずつ渡れるか」という問題を生みました。
オイラー路:グラフのすべての辺をちょうど一度ずつ通る道——「全ての橋を1度ずつ渡る散歩コース」。
定理(オイラー、1736年):
- オイラー閉路(出発点に戻る)が存在 連結かつすべての頂点の次数が偶数——「全ての交差点で入った分だけ出られる」
- オイラー路(起点・終点が異なる)が存在 連結かつ奇数次数の頂点がちょうど2つ
有名な問題「ケーニヒスベルクの橋」(7本の橋をすべて一度ずつ渡れるか?)はオイラーが 1736 年に否定的に解き、グラフ理論の誕生とされます——「答えは NO。なぜなら奇数次数の頂点が4つあるから」。
ハミルトン路
「全ての都市を一度ずつ訪問する旅行ルートを求める」——これがハミルトン路問題です。
ハミルトン路:グラフのすべての頂点をちょうど一度ずつ通る路——「全ての都市を1度ずつ巡る旅程」。
オイラー路と似た問題に見えますが、ハミルトン路の判定は NP 完全問題(多項式時間アルゴリズムが知られていない)です——「解を確認するのは速いが、解を見つけるのは難しい」。
これが有名な「P NP?」問題(100万ドルの懸賞が掛かった難問)との繋がりを持ちます——「コンピュータで効率よく解けるか否かの境界線上にある問題」。
グラフ理論の応用
「グラフ理論は現代のデジタル技術の根幹にある」——毎日使うスマートフォンの地図アプリも、SNSも、グラフ理論で動いています:
| 応用 | グラフの意味 |
|---|---|
| ナビゲーション | 交差点=頂点、道路=辺(重み付き) |
| SNS | ユーザー=頂点、繋がり=辺 |
| コンパイラ | 依存関係グラフ(有向非循環グラフ、DAG) |
| スケジューリング | タスク=頂点、制約=辺 |
| インターネット | ルーター=頂点、リンク=辺 |
| PageRank | Webページのグラフを有向グラフで表現 |
隣接行列と隣接リスト
「グラフをコンピュータで扱うには、表現方法を選ぶ必要がある」:
隣接行列(Adjacency Matrix)
「大きな○×の表でつながりを表す」—— 行列 :(辺 が存在)、(なし)
- 辺の存在確認:——「表を見るだけで一発でわかる」
- メモリ:(疎なグラフには非効率)——「ほとんどが空欄の巨大な表が必要」
隣接リスト(Adjacency List)
「各頂点のお隣さんリストを持つ」——各頂点の隣接頂点リスト
- メモリ:——「辺の数だけのメモリで済む」
- 多くの実用的アルゴリズムはこちらを使用
シリーズのまとめ
このシリーズ「離散数学・数論」では以下を学びました:
| 回 | テーマ |
|---|---|
| 1–3 | 等差・等比数列、Σ公式 |
| 4–5 | 漸化式、数学的帰納法 |
| 6–9 | 約数、GCD、素数、合同式 |
| 10–13 | 行列の演算・逆行列・固有値 |
| 14–15 | フィボナッチ・黄金比、グラフ理論 |
まとめ
- グラフ :頂点と辺の抽象的な構造——「関係を点と線で表す」
- 握手補題:——「辺の本数の2倍が次数の総和」
- DFS・BFS:グラフ探索の基本アルゴリズム、——「全頂点を効率よく訪問できる」
- オイラー路:全辺を通る道(次数の偶奇で判定)——「全ての橋を1度ずつ渡れるか」
- ハミルトン路:全頂点を通る道(NP完全)——「全都市を1度ずつ巡れるか」
- グラフはネットワーク・AI・コンパイラ・SNS に広く応用——「関係性を扱うあらゆる場面で活躍」
離散数学・数論は「コンピュータ科学の言語」です。整数・素数・行列・グラフ——これらは現代のすべてのデジタル技術の根底にあります。