C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で解く「冗長接続(Redundant Connection)」問題 ― Union-Findによる効率的な実装

問題概要

まず、根のない木(unrooted tree)について考えてみましょう。これは閉路(サイクル)を持たない無向グラフのことです。

入力として与えられるのは、もともと N 個のノードを持つ木(ノードの値は 1 から N までの重複しない整数)に、余分な辺を 1 本追加したグラフです。追加された辺は 1 ~ N の中から選ばれた 2 つの異なる頂点を結び、既存の辺とは重複しません。

最終的なグラフは 2 次元配列 edges として渡されます。edges の各要素はペア [u, v](u < v)であり、ノード u と ノード v を結ぶ無向辺を表します。

求めるのは、その辺を取り除けば残りのグラフが N ノードの木になるような辺です。答えが複数存在する場合は、与えられた 2 次元配列の中で最後に現れるものを返す必要があります。返す辺も [u, v](u < v)という同じ形式で表します。

入力例

たとえば、入力が [[1,2], [2,3], [3,4], [1,4], [1,5]] の場合:

C++で解く「冗長接続(Redundant Connection)」問題 ― Union-Findによる効率的な実装

このとき出力は [1,4] となります。辺 [1,4] を削除すると、グラフは再び閉路のない木に戻るためです。

解法のアプローチ:Union-Find(素集合データ構造)

この問題は Union-Find(Disjoint Set Union) と呼ばれるデータ構造を使うことで効率的に解けます。基本的な考え方はシンプルです。

辺を順番に処理していき、ある辺の両端点がすでに同じ連結成分に属している場合、その辺を追加すると閉路が形成されます。つまり、その辺こそが「冗長な接続」だということになります。

アルゴリズムの手順

  • N := 1000 とする
  • サイズ N+5 の配列 parent を定義する
  • サイズ N+5 の配列 rank を定義する
  • 関数 getParent(n) を定義する:
    • parent[n] が -1 なら、n をそのまま返す(n が根であることを意味する)
    • そうでなければ parent[n] = getParent(parent[n]) を返す(経路圧縮により探索を高速化)
  • 関数 unionn(a, b) を定義する:
    • pa := getParent(a)、pb := getParent(b) を計算する
    • pa == pb の場合は false を返す(すでに同じグループ=閉路が発生)
    • rank[pa] > rank[pb] の場合:
      • rank[pa] := rank[pa] + rank[pb]
      • parent[pb] := pa
    • それ以外の場合:
      • rank[pb] := rank[pb] + rank[pa]
      • parent[pa] := pb
    • true を返す(マージ成功)
  • メイン処理では以下を行う:
    • n := edges リストのサイズ
    • i = 0 から n 未満まで繰り返し:
      • parent[edges[i][0]] と parent[edges[i][1]] を -1 に初期化
      • rank[edges[i][0]] と rank[edges[i][1]] を 1 に初期化
    • 配列 ans を定義する
    • 再び i = 0 から n 未満まで繰り返し:
      • u := edges[i][0]、v := edges[i][1]
      • unionn(u, v) が false を返した場合:
        • ans := edges[i](この辺が冗長な辺)
    • ans を返す

すべての辺を最後まで走査することで、仮に複数の候補があっても、配列上で最後に見つかった冗長な辺が自動的に ans に残る仕組みになっています。

C++による実装例

理解を深めるために、以下の実装を見てみましょう。

#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<int> v){
    cout << "[";
    for(int i = 0; i<v.size(); i++){
        cout << v[i] << ", ";
    }
    cout << "]"<<endl;
}
const int N = 1000;
class Solution {
public:
    int parent[N + 5];
    int rank[N + 5];
    int getParent(int n){
        if (parent[n] == -1)
            return n;
        return parent[n] = getParent(parent[n]);
    }
    bool unionn(int a, int b){
        int pa = getParent(a);
        int pb = getParent(b);
        if (pa == pb)
            return false;
        if (rank[pa] > rank[pb]) {
            rank[pa] += rank[pb];
            parent[pb] = pa;
        }
        else {
            rank[pb] += rank[pa];
            parent[pa] = pb;
        }
        return true;
    }
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        int n = edges.size();
        for (int i = 0; i < n; i++) {
            parent[edges[i][0]] = parent[edges[i][1]] = -1;
            rank[edges[i][0]] = rank[edges[i][1]] = 1;
        }
        vector<int> ans;
        for (int i = 0; i < n; i++) {
            int u = edges[i][0];
            int v = edges[i][1];
            if (!unionn(u, v)) {
                ans = edges[i];
            }
        }
        return ans;
    }
};
main(){
    Solution ob;
    vector<vector<int>> v = {{1,2}, {2,3}, {3,4}, {1,4}, {1,5}};
    print_vector(ob.findRedundantConnection(v));
}

入力

{{1,2}, {2,3}, {3,4}, {1,4}, {1,5}}

出力

[1, 4]

まとめ

この手法の計算量は、経路圧縮とランク(サイズ)によるマージのおかげで、ほぼ O(N α(N))(α はアッカーマン関数の逆関数で事実上定数)と非常に高速です。閉路検出が必要なグラフ問題全般に応用できる強力なパターンなので、ぜひ覚えておきましょう。

  1. C++で解くリスのナッツ収集シミュレーション ― 最小移動距離を求めるアルゴリズム

    問題概要 1本の木、1匹のリス、そして複数のナッツがフィールド上にあります。それぞれの位置は2次元グリッドのセルで表現されます。この問題の目的は、リスがすべてのナッツを集めて木の下に1個ずつ運ぶときの最小移動距離を求めることです。 リスの行動には次の制約があります。 一度に持てるナッツは最大1個 移動は上下左右の4方向で、隣接するセルへのみ可能 距離は移動回数(ステップ数)で表される たとえば、入力が「高さ: 5 / 幅: 7 / 木の位置: [2,2] / リスの位置: [4,4] / ナッツ: [[3,0], [2,5]]」の場合、出力は 12 となります。 解法のポイント まず、

  2. C++で解く長方形エリアII ― 座標圧縮と走査線法による被覆面積の計算

    問題概要 軸に平行な長方形のリストが与えられるものとします。各 rectangle[i] = {x1, y1, x2, y2} において、(x1, y1) は i 番目の長方形の左下隅の座標、(x2, y2) は右上隅の座標を表します。 求めたいのは、平面上でこれらすべての長方形が覆っている領域の合計面積です。答えは非常に大きな値になる可能性があるため、109 + 7 で割った余りを返すことになっています。 たとえば、入力が次のような場合を考えてみましょう。 このとき、出力は 6 となります。 解法の方針:座標圧縮+走査線(スイープライン) この問題は、座標圧縮(座標の離散化)と走査線法(