記事ディレクトリ
1.ユニオンサーチの原理
ユニオン検索は、主に要素のグループ化の問題を解決するために使用されます。一連のばらばらなセットを管理し、次の 2 つの操作をサポートします。
- Union : 互いに素な 2 つのセットを 1 つのセットに結合します
- クエリ (検索) : 2 つの要素が同じセットにあるかどうかをクエリします
もちろん、そのような定義はあまりにも学術的であり、それを読んだ後、それが何に使用されているのか理解できないのではないかと心配しています. それでは、最も直接的なアプリケーション シナリオである相対的な問題を確認してみましょう。
説明
家族の一員が多すぎると、2人が親戚であるかどうかを判断するのは本当に簡単ではありません. 相対的な関係図が与えられた場合、無作為に与えられた2人が関連しているかどうかを尋ねます.
ルール: x と y は相対であり、y と z は相対であり、x と z も相対です。x と y が相対の場合、x のすべての相対は y の相対であり、y のすべての相対は x の相対です。入力 (入力)
1 行目: 3 つの整数 n、m、p (n < = 5000、m < = 5000、p < = 5000) は、それぞれ n 人、m 人の親族がいることを示し、p の相対的な関係について尋ねます。次の m 行: 各行には 2 つの数値 Mi、Mj、1<=Mi、Mj<=N があり、Mi と Mj が関連していることを示します。次の p 行: 各行には Pi と Pj の 2 つの数値があり、Pi と Pj が関連しているかどうかを尋ねます。
出力 (出力)
P 行、1 行に 1 つの「はい」または「いいえ」。i 番目のクエリに対する回答が「ある」または「ない」であることを示します。
予備的な分析では、この問題は、グラフ理論で 2 つの点が同じ連結部分グラフにあるかどうかを判断する問題であると考えられます。すべての人をバラバラなセットに分割するモデルを構築できます。各セットの人々は互いに親戚です。2 人が親戚かどうかを判断するには、同じセットに属しているかどうかを確認します。したがって、ここでは、メンテナンスのためにユニオン チェック セットを使用することを検討できます。
2.ユニオンサーチの導入
集合探索の重要な考え方は、集合内の要素で集合を表現することです。コレクションをギャングに例える興味深い比喩を見たことがありますが、代表的な要素はギャングのリーダーです。次に、この類推を使用して、マージ セットがどのように機能するかを見てみましょう。
当初、すべてのヒーローは独自の方法で戦っていました。それぞれのリーダーは、当然のことながら自分自身です。(要素が1つしかない集合の場合、代表要素は当然唯一のもの)
ここで、No.1 と No.3 が競争し、No.1 が勝つと仮定すると (ここではどちらが勝ってもかまいません)、No.3 は No.1 をリーダーとして認識します (No.1 と No.3 がいるセットをマージします)。 No.1が代表要素)
今、No. 2 は No. 3 と競争したい (No. 3 と No. 2 があるセットをマージする) が、No. 3 は言った、私と戦うな、主があなたをきれいにするのを手伝わせて (代表的な要素をマージします)。今回も1号が勝ったとしましょうので、2号も1号をリーダーとして認識しています。
ここで、4 号、5 号、6 号にもギャングの合併があり、河川や湖沼の状況が次
のようになったとします。私が今言ったことは、ギャングリーダーのNo. 1とNo. 4に電話して、フレームと戦ってください(本当に一生懸命マスターするのを手伝ってください)。1号の勝利後、4号は1号をリーダーと認め、もちろん部下も降伏した。
さて、比喩は終わりました。グラフ理論の基礎を少しお持ちの方は、これが木のような構造であることにお気付きだと思います. 集合の代表的な要素を見つけるには、親ノード (図の矢印が指す円) にアクセスするだけで済みます.レイヤーごとにツリーのルート ノード (図のオレンジ色の円) に直行します。ルート ノードの親はそれ自体です。それをツリーとして直接描画できます。
このようにして、union-find コードの最も単純なバージョンを書くことができます。
3.パス圧縮
最も単純な結合と検索の効率は比較的低くなります。たとえば、次のシナリオを考えてみましょう。
ここで、merge(2,3) を実行したいので、2 から 1 を見つけ、fa[1]=3 となるため、次のようになります。
次に、別の要素 4 を見つけ、merge(2,4) を実行する必要があります。
from 2 1 を見つけて 3 を見つけて fa[3]=4 となるので、
誰もが感じているはずです。これは長い鎖を形成する可能性があり、鎖がどんどん長くなるにつれて、下 ルートノードを見つけるのがますます難しくなっています。
それを解決する方法は?パス圧縮方式を使用できます。要素に対応するルート ノードのみに関心があるため、各要素からルート ノードへのパスをできるだけ短くする必要があります。次のように、できれば 1 ステップだけにする必要があります。
実際、これも達成するのに非常に適しています。クエリを実行している限り、途中で各ノードの親ノードをルート ノードとして設定できます。次回検索するときに多くの手間を省くことができます。
しかし実際には、パスの圧縮はクエリ時にのみ実行され、1 つのパスのみが圧縮されるため、ユニオン検索の最終的な構造は依然として複雑になる可能性があります。
たとえば、1 要素セットとマージする必要がある、より複雑なツリーがあります。
この時点でmerge(7,8)したい場合、選べるなら7の親ノードを8にするか、8の親ノードを7にするか。
もちろん後者。7 の親ノードを 8 にすると、ツリーの深さ (ツリーで最も長いチェーンの長さ) が深くなり、元のツリーの各要素からルート ノードまでの距離が長くなるため、そして、ルートノードを見つけます.パスはそれに応じて長くなります. パス圧縮がありますが、パス圧縮にも時間がかかります。また、親ノードを 8 から 7 に設定すると、関連のないノードには影響しないため、この問題は発生しません。
これは私たちにインスピレーションを与えます: 単純なツリーを複雑なツリーにマージするべきであり、その逆ではありません。このようにマージした後、ルートノードまでの距離が長くなるノードの数は比較的少ないためです。
4.全体の実現
#pragma once
#include<vector>
#include<algorithm>
using namespace std;
class UnionFindSet
{
public:
UnionFindSet(size_t n)
:_ufs(n, -1)
{
}
//合并
void Union(int x1, int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
//数据量小的集合往数据量大的集合合并
//if (abs(_ufs[root1]) < abs(_ufs[root2]))
//{
// swap(root1, root2);
//}
//如果root1和root2相等,说明在一个集合,就没必要进行合并了
if (root1 != root2)
{
//将root2合并到root1,root1集合的个数需要加上root2集合的个数
_ufs[root1] += _ufs[root2];
//将root1作为root2的根
_ufs[root2] = root1;
}
}
//找根节点
int FindRoot(int x)
{
int root = x;
while (_ufs[root] >= 0)
{
root = _ufs[root];
}
//路径压缩
//当前值到根路径上的所有值都进行压缩
while (_ufs[x] >= 0)
{
//保存当前数值的父亲
int parent = _ufs[x];
_ufs[x] = root;
x = parent;
}
return root;
}
bool Inset(int x1, int x2)
{
return FindRoot(x1) == FindRoot(x2);
}
//获取元素个数
size_t SetSize()
{
int count = 0;
for (size_t i = 0; i < _ufs.size(); ++i)
{
if (_ufs[i] < 0)
{
count++;
}
}
return count;
}
private:
vector<int> _ufs;
};