【第12回】グラフの復習とUnion-Find

BFS、DFSをしっかり復習しよう

Homeブログ一覧【第12回】グラフの復習とUnion-Find

グラフの復習

前回行ったBFS、DFSについて復習しましょう。
以下の点を抑えていただければ、グラフの問題を解く際に役立つかと思います。

  • グラフの辺と頂点を2次元配列で管理すること
  • 探索を行う際に、起点にしている頂点と次に探索する頂点を意識すること
  • BFS、DFSの違いを理解すること

BFS(幅優先探索)

  • 最短経路の発見が得意。
  • 先に見つけた頂点から探索。
  • queueを使って実装する。

DFS(深さ優先探索)

  • より深い頂点を優先して探索。
  • stackを使って実装する。

グラフ探索のテンプレート

BFS(幅優先探索)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;

    vector<vector<int>> g(n);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u); // 無向グラフの場合
    }

    vector<int> dist(n, -1);
    queue<int> q;

    int start = 0; // 開始点を0とする
    dist[start] = 0;
    q.push(start);

    while (!q.empty()) {
        int v = q.front();
        q.pop();

        for (int i = 0; i < g[v].size(); i++) {
            int next = g[v][i];
            if (dist[next] == -1) {
                dist[next] = dist[v] + 1;
                q.push(next);
            }
        }
    }

    // 距離配列の出力をする場合
    for (int i = 0; i < n; i++) {
        cout << i << ' ' << dist[i] << endl;
    }

    return 0;
}

グリッドを探索する場合
#include <bits/stdc++.h>
using namespace std;

// 移動方向(上下左右)
const vector<int> dx = {1, -1, 0, 0};
const vector<int> dy = {0, 0, 1, -1};

int main() {
    int h, w;
    cin >> h >> w;

    vector<string> grid(h);
    for (int i = 0; i < h; i++) {
        cin >> grid[i];
    }

    vector<vector<int>> dist(h, vector<int>(w, -1));
    queue<pair<int, int>> q;

    // 0,0を開始点として設定
    dist[0][0] = 0; 
    q.push({0, 0});

    while (!q.empty()) {
        auto [x, y] = q.front();
        q.pop();

        // 上下左右の隣接セルを探索
        for (int dir = 0; dir < 4; dir++) {
            int nx = x + dx[dir];
            int ny = y + dy[dir];

            // グリッドの範囲内かつ未訪問で'.'のセルならば進む
            if (nx >= 0 && nx < h && ny >= 0 && ny < w && grid[nx][ny] == '.' && dist[nx][ny] == -1) {
                dist[nx][ny] = dist[x][y] + 1;
                q.push({nx, ny});
            }
        }
    }

    // 距離配列の出力をする場合
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (dist[i][j] == -1) {
                cout << "# ";
            } else {
                cout << dist[i][j] << " ";
            }
        }
        cout << endl;
    }

    return 0;
}

DFS(深さ優先探索)

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;

    vector<vector<int>> g(n);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u); // 無向グラフの場合
    }

    vector<int> dist(n, -1);
    stack<int> s;

    int start = 0; // 開始点を0とする
    dist[start] = 0;
    s.push(start);

    while (!s.empty()) {
        int v = s.top();
        s.pop();

        for (int i = 0; i < g[v].size(); i++) {
            int next = g[v][i];
            if (dist[next] == -1) {
                dist[next] = dist[v] + 1;
                s.push(next);
            }
        }
    }

    // 距離配列の出力をする場合
    for (int i = 0; i < n; i++) {
        cout << i << ' ' << dist[i] << endl;
    }

    return 0;
}

グリッドを探索する場合
#include <bits/stdc++.h>
using namespace std;

// 移動方向(上下左右)
const vector<int> dx = {1, -1, 0, 0};
const vector<int> dy = {0, 0, 1, -1};

int main() {
    int h, w;
    cin >> h >> w;

    vector<string> grid(h);
    for (int i = 0; i < h; i++) {
        cin >> grid[i];
    }

    vector<vector<int>> dist(h, vector<int>(w, -1));
    stack<pair<int, int>> s;

    // 0,0を開始点として設定
    dist[0][0] = 0; 
    s.push({0, 0});

    while (!s.empty()) {
        auto [x, y] = s.top();
        s.pop();

        // 上下左右の隣接セルを探索
        for (int dir = 0; dir < 4; dir++) {
            int nx = x + dx[dir];
            int ny = y + dy[dir];

            // グリッドの範囲内かつ未訪問で'.'のセルならば進む
            if (nx >= 0 && nx < h && ny >= 0 && ny < w && grid[nx][ny] == '.' && dist[nx][ny] == -1) {
                dist[nx][ny] = dist[x][y] + 1;
                s.push({nx, ny});
            }
        }
    }

    // 距離配列の出力をする場合
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (dist[i][j] == -1) {
                cout << "# ";
            } else {
                cout << dist[i][j] << " ";
            }
        }
        cout << endl;
    }

    return 0;
}


演習問題

以下の問題から2~3問解いて、グラフ探索の復習をしてみましょう。

Union-Find

Union-Findで出来ること

  • 要素をグループに分ける
  • グループをまとめる
  • 2つの要素が同じグループに属しているかどうかを判定する
  • 上記の操作を高速に行える。

計算量は、O(α(N))O(\alpha(N))で、α(N)\alpha(N)はアッカーマン関数の逆関数で、ほぼ定数時間で処理できる。O(logN)O(\log N)以下と考えても問題ない。

木構造について

Union-Findは木構造を使ってグループを管理します。 木構造とは、要素を頂点とし、要素間の関係を辺で表現したデータ構造です。 すべての頂点が少なくとも1つの辺でつながっており、閉路(輪になっている部分)が存在しないグラフです。N個の頂点を持つ木構造はN-1本の辺を持ちます。

Union-Findの実装

#include <bits/stdc++.h>
using namespace std;

// Union-Findで用いる配列を宣言
// parent[i]はiの親の番号を表す
// treeRank[i]はiを根とする木の高さを表す
vector<int> parent, treeRank;

// 初期化: N個の要素でUnion-Findを初期化する
void init(int n) {
    parent.resize(n);
    treeRank.resize(n, 1);
    for (int i = 0; i < n; ++i) parent[i] = i;
}

// find関数: xの属する集合の根を見つける(経路圧縮)
int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]); // 経路圧縮
    }
    return parent[x];
}

// unite関数: 2つの要素xとyの属する集合を結合する
void unite(int x, int y) {
    int rootX = find(x);
    int rootY = find(y);

    if (rootX != rootY) {
        if (treeRank[rootX] > treeRank[rootY]) {
            parent[rootY] = rootX;
        } else if (treeRank[rootX] < treeRank[rootY]) {
            parent[rootX] = rootY;
        } else {
            parent[rootY] = rootX;
            treeRank[rootX]++;
        }
    }
}

// same関数: 2つの要素xとyが同じ集合に属するかを判定
bool same(int x, int y) {
    return find(x) == find(y);
}

int main() {
    int N, Q;  // N: 頂点数, Q: クエリ数
    cin >> N >> Q;

    // Union-FindをN個の頂点で初期化
    init(N);

    for (int i = 0; i < Q; ++i) {
        int type, A, B;
        cin >> type >> A >> B;

        if (type == 0) {
            // 連結クエリ(type = 0)の場合
            unite(A, B);
        } else {
            // 判定クエリ(type = 1)の場合
            if (same(A, B)) {
                cout << "Yes" << endl;
            } else {
                cout << "No" << endl;
            }
        }
    }

    return 0;
}

classを使った実装
#include <bits/stdc++.h>
using namespace std;

// Union-Findのクラス定義
class UnionFind {
private:
    vector<int> parent, rank;

public:
    // n個の要素でUnion-Findを初期化する
    UnionFind(int n) : parent(n), rank(n, 1) {
        // 自分を親として初期化
        for (int i = 0; i < n; ++i) parent[i] = i;
    }

    // find関数: xの属する集合の根を見つける
    // 経路圧縮を使って、探索したノードの親を根に直接つなぐ
    int find(int x) {
        // 自分自身が親でない場合、再帰的に親を見つける
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // 親を根に変更(経路圧縮)
        }
        return parent[x];  // 最終的な根を返す
    }

    // unite関数: 2つの要素xとyの属する集合を結合する
    void unite(int x, int y) {
        // それぞれの根を見つける
        int rootX = find(x);
        int rootY = find(y);

        // 異なる集合なら結合する
        if (rootX != rootY) {
            // ランクを使って、高い木に低い木を接続することで木の高さを抑える
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;  // rootYの親をrootXに設定
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;  // rootXの親をrootYに設定
            } else {
                // ランクが同じ場合はrootYをrootXに接続し、rootXのランクを1増やす
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }

    // same関数: 2つの要素xとyが同じ集合に属するかを判定
    bool same(int x, int y) {
        return find(x) == find(y);  // 根が同じなら同じ集合に属する
    }
};

int main() {
    int N, Q;  // N: 頂点数, Q: クエリ数
    cin >> N >> Q;

    // Union-Findのインスタンスを作成し、N個の頂点を管理する
    UnionFind uf(N);

    for (int i = 0; i < Q; ++i) {
        int type, A, B;
        cin >> type >> A >> B;  // クエリの種類と対象の頂点A, Bを入力

        if (type == 0) {
            // 連結クエリ(type = 0)の場合
            // 頂点Aと頂点Bの間に辺を追加(同じ集合に結合)
            uf.unite(A, B);
        } else {
            // 判定クエリ(type = 1)の場合
            // 頂点Aと頂点Bが連結かどうかを判定
            if (uf.same(A, B)) {
                // 連結であれば"Yes"を出力
                cout << "Yes" << endl;
            } else {
                // 連結でなければ"No"を出力
                cout << "No" << endl;
            }
        }
    }

    return 0;
}

演習問題