木とグラフ — 隣接リスト・DFS・BFSの使い分け
この章の目次開く
データ構造編の最後は木とグラフです。「グラフ問題は難しそう」と身構える人が多いのですが、コーディングテストで必要な道具は驚くほど少ない——**持ち方1つ(隣接リスト)と探索2つ(DFS/BFS)**だけです。
グラフを構成する語彙
グラフは**頂点(ノード)と、頂点同士をつなぐ辺(エッジ)**でできています。
- 無向グラフ — 辺に向きがない(友達関係)
- 有向グラフ — 辺に向きがある(フォロー関係)
- 木 — 閉路のない連結グラフ。N頂点なら辺はN-1本
- 連結 — すべての頂点が辺をたどって行き来できる状態
問題文では「N個の都市とM本の道路」「人物AとBは友人」のような姿で登場します。「〇〇と△△がつながっている」という関係の集まりが出たらグラフです。
グラフの持ち方: 隣接リスト
コードでグラフを扱うには、まず入力をデータ構造に変換します。実戦ではほぼ一択で隣接リスト——「各頂点について、隣の頂点の一覧を持つ」形式を使います。
// N頂点、辺のリスト edges = [[0,1], [0,2], [1,3]] (無向)
const graph: number[][] = Array.from({ length: N }, () => []);
for (const [a, b] of edges) {
graph[a].push(b);
graph[b].push(a); // 無向なら両方向に張る(有向ならこの行を消す)
}
// graph[0] → [1, 2] … 頂点0の隣は1と2
学習者N×Nの表(隣接行列)で持つ方法を本で見たことあるけど、そっちじゃダメなの?
隣接行列はN ≤ 1,000程度なら使えますが、N = 10^5 だと10^10マスでメモリが即死します。辺の数Mに比例したメモリで済む隣接リストが実戦の標準です。この5行の変換はグラフ問題の儀式として体に入れてしまいましょう。
DFS — 行けるところまで潜る
**DFS(深さ優先探索)**は「行き止まりまで進み、戻って別の道へ」という探索です。再帰で書くのが最も素直です。
問題: N人の人物とM組の友人関係がある。友人の友人まで含めた「グループ」はいくつあるか。(連結成分の数)
function countGroups(N: number, graph: number[][]): number {
const visited = new Array(N).fill(false);
const dfs = (v: number) => {
visited[v] = true;
for (const next of graph[v]) {
if (!visited[next]) dfs(next); // 未訪問の隣へ潜る
}
};
let groups = 0;
for (let v = 0; v < N; v++) {
if (!visited[v]) {
groups++; // 未訪問の頂点 = 新しいグループの発見
dfs(v); // そのグループ全体を訪問済みにする
}
}
return groups;
}「全頂点を起点候補にして、未訪問なら成分カウント+1して塗りつぶす」——連結成分カウントはグラフ問題の登竜門で、paiza・AtCoder両方で頻出です。
再帰の深さに注意
Node.jsの再帰は深さ1万〜数万程度でスタックオーバーフローします。N = 10^5 の一本道グラフだと危険です。その場合は、第5章のスタックを使った非再帰DFSに書き換えます。
const dfsIterative = (start: number) => {
const stack = [start];
visited[start] = true;
while (stack.length > 0) {
const v = stack.pop()!;
for (const next of graph[v]) {
if (!visited[next]) {
visited[next] = true;
stack.push(next);
}
}
}
};キューに変えればBFSになる——という対称性に気づくと、2つの探索が1つの型として整理できます。
BFS — 近い順に広がる
BFS(幅優先探索)は、スタートから「距離1の頂点→距離2の頂点→…」と近い順に探索します。第5章のキュー(添字方式)がここで主役になります。
BFSの最大の武器は、辺の重みがすべて等しいとき、最初に到達した時点の距離が最短距離だと保証されることです。
問題: 頂点0から各頂点への最短距離(辺の本数)を求めよ。
function bfsDistances(N: number, graph: number[][], start: number): number[] {
const dist = new Array(N).fill(-1); // -1 = 未到達
const queue: number[] = [start];
let head = 0;
dist[start] = 0;
while (head < queue.length) {
const v = queue[head++];
for (const next of graph[v]) {
if (dist[next] === -1) { // distがvisited判定を兼ねる
dist[next] = dist[v] + 1;
queue.push(next);
}
}
}
return dist;
}dist 配列が訪問済みチェックと距離記録を兼ねるのがポイントです。前章のグリッド探索も、このBFSの「頂点=マス、辺=上下左右」版でした。迷路・グリッドの最短手数はBFSが絶対の定番です。
DFSとBFSの使い分け

| 求めたいもの | 選ぶ探索 | 理由 |
|---|---|---|
| 最短距離・最小手数(重みなし) | BFS | 近い順に広がるので最初の到達が最短 |
| 連結成分・到達可能性 | どちらでも | 塗りつぶせれば十分 |
| 経路の全列挙・バックトラッキング | DFS(再帰) | 「戻る」動きが再帰と相性◎ |
| 木の集計(部分木のサイズなど) | DFS(再帰) | 帰りがけに子の結果を合成できる |
迷ったときの合言葉は「最短ならBFS、全部見るだけならどちらでも、列挙ならDFS」です。
木の問題はDFSの帰りがけが鍵
木(閉路なし)の問題では、「子の結果を親でまとめる」集計が頻出です。再帰DFSの戻り値を使うと自然に書けます。
// 各頂点を根とする部分木のサイズを求める
const size = new Array(N).fill(0);
const dfs = (v: number, parent: number): number => {
let s = 1; // 自分自身
for (const c of graph[v]) {
if (c !== parent) s += dfs(c, v); // 子の部分木サイズを合算
}
return (size[v] = s);
};
dfs(0, -1);木は閉路がないので、visited配列の代わりに「親に戻らない」(c !== parent)だけで探索が成立します。
この章のパターンの見分け方
| 問題文のシグナル | 使う道具 |
|---|---|
| 「AとBがつながっている」「友人関係」「道路」 | まず隣接リスト化 |
| 「グループはいくつ」「到達できるか」 | DFS/BFSで塗りつぶし(連結成分) |
| 「最短」「最小手数」「最少ターン」(重みなし) | BFS |
| 「迷路」「グリッドで上下左右」 | マスを頂点とみなしてBFS |
| 「部分木の〇〇」「子の合計」 | 再帰DFSの戻り値集計 |
メンターグラフ問題の8割は「隣接リストに変換 → DFSかBFSを貼る」で骨格が完成します。残る2割——辺に重みがある最短路や、つながりの管理を高速化したい場面——は第11章のダイクストラ法とUnion-Findで拾います。
まとめ
- グラフは「関係の集まり」。入力を隣接リストに変換するのが儀式
- DFSは潜って戻る探索。再帰が素直だが、深いグラフでは非再帰版(スタック)に切り替える
- BFSは近い順の探索。重みなし最短距離の絶対定番。キューは添字方式で
- 使い分けは「最短ならBFS、列挙ならDFS、塗りつぶしはどちらでも」
- 木の集計は再帰DFSの戻り値で「子→親」に情報を流す
これでデータ構造編は完了です。次章からPart 3・解法パターン編に入ります。まずはすべての解法の出発点、「全探索」からです。