{ }codelper

学ぶ › C++

アルゴリズム · 32

グラフと深さ優先探索

誰と誰がつながっているかを書きとめ、行ける所をもれなくたどります。

グラフは、点(人、町、部屋)とそれらを結ぶ線(友だち関係、道、ドア)でできたデータです。
下の例は友だちの組を読んで人ごとの友だちリストを作り、1 番から友だちをたどって着いた順番と、たがいに行き来できる人どうしをまとめたグループの数を表示します。

#include <iostream>#include <vector> std::vector<std::vector<int>> adj; // adj[v] = v번의 친구들std::vector<bool> seen;std::vector<int> order; // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요void dfs(int v) {    seen[v] = true;    order.push_back(v);    for (int w : adj[v]) {        if (!seen[w]) dfs(w);    }} int main() {    int n, m;    std::cin >> n >> m;    adj.resize(n + 1);    for (int i = 0; i < m; i++) {        int a, b;        std::cin >> a >> b;        adj[a].push_back(b); // 서로 친구라 양쪽에 넣어요        adj[b].push_back(a);    }     seen.assign(n + 1, false);    dfs(1);    for (size_t i = 0; i < order.size(); i++) std::cout << (i > 0 ? " " : "") << order[i];    std::cout << '\n';     // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나    seen.assign(n + 1, false);    int groups = 0;    for (int s = 1; s <= n; s++) {        if (seen[s]) continue;        groups++;        seen[s] = true;        std::vector<int> stack = {s};        while (!stack.empty()) {            int v = stack.back();            stack.pop_back();            for (int w : adj[v]) {                if (!seen[w]) {                    seen[w] = true;                    stack.push_back(w);                }            }        }    }    std::cout << groups << '\n';}
stdin
7 5
1 2
1 3
2 4
3 4
5 6
stdout
1 2 4 3
3

ポイント

  • 入力はふつう「a b」の組で来ます。
    これを人ごとに隣をまとめたリスト adj[v] に直しておきます(隣接リスト)。
    そうすれば「v の隣は誰か」が、組を見直さなくてもすぐ分かります。
    両方向につながる関係なら、adj[a] に b を、adj[b] に a を、両方とも入れます。
  • 番号が 1 から始まるなら、n + 1 個の場所を作って 0 番は空けておきます。
    番号をそのまま位置として使えます。
  • 深さ優先探索(DFS)は、今いる所に訪問済みの印(seen)を付け、まだ行っていない隣が出てきたらすぐそちらへ入ります。
    行き先がなくなったら来た所へ戻り、残りの隣を見ます。
    例の dfs(v) は番号 v だけを受け取り、adj・seen・order は関数の外に置いたものを共有します。
    例では 1 番の友だちは 2 番と 3 番ですが、2 番に入ったついでに 4 番まで行ってからやっと 3 番を見るので、順番は 1 2 4 3 です。
  • 印があるので、一人には一度しか入らず、友だち関係一つは両側から一度ずつしか見ません。
    だから仕事の量は、人数に組の数を足したぶんです。
  • 再帰の代わりにスタックを自分で使ってもかまいません。
    出発点を入れ、一つ取り出して、まだ行っていない隣に印を付けながら入れる、ということをスタックが空になるまで繰り返します。
    たどる順番は変わることがありますが(例をスタックで回すと 1 3 4 2)、着く人は同じです。
  • まだ印のない人から探索を新しく始めるたびに、グループが一つ増えます。
    例は {1, 2, 3, 4}、{5, 6}、{7} なので 3 です。

言語ごとの書き方

Pythonadj = [[] for _ in range(n + 1)]、adj[a].append(b)。
スタックはリストの append と pop() です
JavaScriptArray.from({ length: n + 1 }, () => [])、adj[a].push(b)。
スタックは配列の push と pop() です
TypeScriptJavaScript と同じです。
型は number[][]。
stack.pop() の型は number | undefined なので、空でないと確かめた場所では stack.pop()! と書きます
JavaArrayList<ArrayList<Integer>> に空の ArrayList を n + 1 個 add しておき、adj.get(a).add(b)。
スタックは ArrayDeque の push と pop です
C#new List<int>[n + 1] は場所がすべて null なので、一つずつ new List<int>() を入れます。
そのあと adj[a].Add(b)。
スタックは Stack<int> の Push と Pop です
C++std::vector<std::vector<int>> adj(n + 1) — 空のベクターが n + 1 個、別々にできます。
adj[a].push_back(b)。
スタックは vector の push_back、back()、pop_back() で作ります
C伸びるリストがありません。
int adj[MAX][MAX] と隣の数 deg[v] を置き、adj[a][deg[a]++] = b で入れます。
スタックも配列と番号 top で作ります
Craykon新リスト(n + 1, []) は場所ごとに別々の空リストを作ります。
adj[a].追加(b)、隣は それぞれ (w の中 adj[v])。
スタックにはリストの末尾を使います。
stack.追加(w) で入れ、stack[stack.個数 - 1] を読んでから stack.位置で取り除く(stack.個数 - 1) で取り除きます

よくある間違い

訪問済みの印を付け忘れた

void dfs(int v) {
    order.push_back(v);
    for (int w : adj[v]) dfs(w);
}

→ 1 番と 2 番が友だちというだけで 1 → 2 → 1 → 2 … と果てしなく入り続け、スタックがあふれて SIGSEGV で落ちます。同じミスをすると、Python は RecursionError、JavaScript は RangeError、Java は StackOverflowError で止まり、C# は Stack overflow. と表示して落ち、Craykon は「関数の呼び出しが深すぎるので止めました」で止まります

入ったらすぐ seen[v] = true と印を付け、印のない隣にだけ入ります。

隣のリストを [[]] * n で作ったPython · JavaScript · TypeScript

adj = [[]] * 3
adj[1].append(2)
print(adj)

→ [[2], [2], [2]] — 空のリスト一つを三つの場所がいっしょに指しているので、1 番に入れた友だちが全員に入ります。JavaScript の new Array(3).fill([]) も同じです

場所ごとに新しく作ります:[[] for _ in range(n + 1)]、Array.from({ length: n + 1 }, () => [])。

配列だけ作って、中にリストを入れなかったJava · C#

List<int>[] adj = new List<int>[3];
adj[1].Add(2);

→ NullReferenceException — 配列を作っただけでは、場所はすべて null です。Java で new ArrayList[3] と作っても、同じように NullPointerException で止まります

使う前に、ループで一つずつ adj[v] = new List<int>();(Java は new ArrayList<>())を入れます。

言語ごとの違い
再帰の DFS は短く書けますが、人が一列に長くつながっていると、そのぶん深く入ります。
ジャッジでは、例の Python コードは 900 人が一列につながった入力は解けますが、1,100 人では RecursionError で止まります。
Craykon のコードは 1,100 人は解けますが、5,000 人で止まります。
n が数千を超えうるなら、最初からスタックで書きましょう。

練習問題