そしてチェックセットとその実装

概要

定義:
ユニオン検索セットはツリー型のデータ構造であり、いくつかの素なセット (つまり、いわゆるユニオンと検索) のマージとクエリの問題を処理するために使用されます。たとえば、ユニオン検索を使用して、フォレスト内にツリーが何本あるか、特定のノードが特定のツリーに属しているかどうかなどを判断できます。

主なコンポーネント:
ユニオン検索は主に、整数配列 pre[ ] と 2 つの関数 find( ) および join( ) で構成されます。
配列 pre[] は各ポイントの先行ノードが誰であるかを記録し、関数 find(x) は指定されたノード x が属するセットを見つけるために使用され、関数 join(x,y) は 2 つのノード x をマージするために使用されます。そしてy。

機能:
ユニオン検索の主な機能は、接続されているブランチの数を見つけることです。グラフ内のすべての点が (直接的または間接的に接続されて) 到達可能である場合、このグラフの接続された分岐の数は 1 になります。このグラフにすべて到達可能な 2 つの主要なサブグラフがある場合、このグラフの接続された分岐の数は 2 になります。 ..

Luogu に関するテンプレートの質問は次のとおりです。

P3367 [テンプレート] マージ検索 - Luogu | コンピューター サイエンス教育の新しいエコロジー (luogu.com.cn)

find( ) 関数の定義と実装

最初に配列を定義する必要があります: int pre[1000];(配列の長さは質問の意味によって異なります)。この配列は、各ノードの親ノードが誰であるかを記録します。ノードには 0 または 1 から始まる番号が付けられます (質問の意味に応じて)。たとえば、pre[16]=6ノード 16 の親ノードは番号 6 であることを意味します。ノードの親ノードがそれ自体である場合、それはルート ノードであることを意味し、検索はここで終了します。孤独で自己完結型、つまり一点であり、その親ノードが自分自身である人もいます。
プリ配列には各ノードの親ノードのみが記録されるため、ノードの祖先ノード (ノードが配置されているツリーのルート ノード) を知りたい場合は、レベルごとに確認するしかありません。したがって、ルート ノードを見つけるために find(x) 関数が使用されていることがわかります。
この関数の具体的な実装を以下に示します。

int find(int x) //查找节点x所在树的根节点
{
    if (pre[x] == x)
        //约定根节点的前驱还是自己,所以x的父节点和自己相同时即可返回
        return x;
    return find(pre[x]); //小递归
    /*
    等同于:
    while(pre[x]!=x)
        x=pre[x]
    return x;
    */
}

join() 関数の定義と実装

2 つのコレクションをマージする場合、どうすれば実現できますか? いくら変えるの?実際、これは非常に簡単です。一方のツリーを他方のツリーのルート ノードの下に吊るすだけです。つまり、一方のツリーのルート ノードの前任者を他方のツリーのルート ノードとして設定するだけです。 2 つのツリーを変更する必要はありません。join() 関数の役割はこれを実現することです。

join(x,y) の実行ロジックは次のとおりです。

  1. x が存在するツリーのルート ノードを見つけます。
  2. y が存在するツリーのルート ノードを見つけます。
  3. x と y が等しくない場合は、一方のツリーのルート ノードを他方のツリーのルート ノードの親ノードとしてランダムに選択し、x と y のマージを完了します。

この関数の具体的な実装を以下に示します。

void join(int x, int y)
{
    //查找x和y的根节点
    int fx = find(x), fy = find(y);
    //如果根节点不同,即处于不同的树,将fx的父节点设为fy,即将x所在子树加到y所在树的根节点下
    if (fx != fy)
        pre[fx] = fy;
}

パス圧縮アルゴリズムの 1 つ (find() 関数の最適化)

問題の導入:
前に紹介した join(x,y) は、実際には、異なるノードをマージする方法を提供します。通常、ループと組み合わせて、与えられた大量のデータをいくつかの大きなセット (つまり、結合検索セット) に結合できます。しかし、問題も発生します。このコードを見てみましょう。

if(fx != fy)  
	pre[fx]=fy;

誰が誰の前身であるかについて明確なルールはありませんが、次のデータを前のデータの前身として直接指定します。これにより、予測不可能な最終ツリー構造が得られます。つまり、それは良好な n 分ツリーである可能性もあれば、単一分岐ツリー構造 (ロング スネーク) である可能性もあります。想像してみてください。実際に最終的に単一のツリー構造が形成される場合、その効率は非常に低くなります (ツリーの深さが深すぎると、必然的にクエリ処理に時間がかかります)。
そして、私たちの理想的な状況は、全員の直属の上位ノードがルート ノードであるため、ツリー構造全体が 2 つのレベルのみになり、現時点ではルート ノードへのクエリは 1 回だけで済むことです。したがって、これはパス圧縮アルゴリズムにつながります。

実現:
上記のクエリ プロセスから、ルート ノードを見つけるためにノードから開始する場合、一連のノードを通過することは難しくありません。これらのノードのうち、ルート ノードを除くすべてのノードを変更する必要があります。直前のノードがルート ノードです。したがって、この考えに基づいて、再帰的メソッドを使用して、レイヤーごとに返すときにノードの
直接の先行者 (つまり、値) を変更できます。pre[x]簡単に言うと、xからルートノードまでの経路上のすべての点のpre(上位)をルートノードとして設定することです。具体的な実装コードを以下に示します。

int find(int x)
{
    if (pre[x] == x)
        return x;
    return pre[x] = find(pre[x]);
}

このアルゴリズムには欠陥があります。特定のノードの代表要素が見つかった後でのみ、検索パス上の各ノードに対してパス圧縮を実行できます。つまり、ルックアップ操作が初めて実行されるときと、その後のみ圧縮効果はありません。

パス圧縮アルゴリズム 2 (加重マーキング法)

備考:
実際、これもパス圧縮アルゴリズムですが、考え方がより高度である (考えるのが簡単ではない) という違いがあります。

主な考え方:
加重表記法では、ノードが配置されているツリーの高さを示すために、ツリー内のすべてのノードに重みを追加する必要があります (たとえば、rank[x]=3 は、ノードが配置されているツリーの高さを意味します) x ノードの位置は 3)。このようにして、操作をマージするときに、この重みのサイズを使用して、誰が誰の上位であるかを決定できます。
マージ操作では、マージする 2 つの集合の代表を x と y とすると、pre[x] = yまたは を作成するだけで済みますpre[y] = xただし、マージされたツリーの劣化を防ぐため (つまり、ツリー内の左右のサブツリーの深さの違いをできるだけ小さくするため)、要素 x ごとに、rank[x] 配列を追加して、サブツリー x 高。マージする場合、 if rank[x] < rank[y]、 then order pre[x] = y、そうでない場合 order pre[y] = x

たとえば、A と F で表されるセットに対してマージ操作を実行します (次の図を参照)。

ここに画像の説明を挿入

ランク(A) > ランク(F) なので、pre[F]= A とします。結合されたグラフを以下に示します。

ここに画像の説明を挿入

マージ前の 2 つのツリーの最大高さは 3 で、マージ後も 3 のままであり、目標を達成していることがわかります。ただし、pre[A] = F の場合、結合されたツリーの合計の高さは増加します。

魚と熊の手の両方を持つことはできないとよく言いますが、同様に、時間の複雑さと空間の複雑さを両方持つことは困難です。ツリー内のノードの高さをマークするために各ノードに重みが追加されるため、重み情報を保存するには追加のデータ構造が必要となり、追加のスペース オーバーヘッドが発生します。

実装:
加重表記法の中核は、ランク配列の論理制御にあります。主な状況は次のとおりです。

  1. ランク[x] < ランク[y]の場合、pre[x] = y とします。
  2. ランク[x] == ランク[y]の場合、上位を任意に指定できます。
  3. ランク[x] > ランク[y]の場合、pre[y] = x とします。

実際にコードを書くときは、コードをできるだけ簡潔にするために、論理的な選択肢としてポイント 1 だけを取り上げ、その他の選択肢としてポイント 2 とポイント 3 を取ることができます (とにかく、ポイント 2 は上位を任意に指定します)。具体的なコードは以下のようになります。

void join(int x, int y)
{
    x = find(x); //寻找 x的代表元
    y = find(y); //寻找 y的代表元
    if (x == y)  //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,直接返回;否则,执行下面的逻辑
        return;
    if (height[x] > height[y]) //如果 x的高度大于 y,则令 y的上级为 x
        pre[y] = x;
    else
    {
        if (height[x] == height[y]) //如果 x的高度和 y的高度相同,则令 y的高度加1
            height[y]++;
        pre[x] = y; //让 x的上级为 y
    }
}

要約する

1. セット内の要素を使用してセットを表すと、この要素はセットの代表要素と呼ばれます;
2. セット内のすべての要素は、代表要素をルートとしてツリー構造に編成されます;
3. それぞれについて要素 x、pre[x] は、ツリー構造内の x の親ノードを格納します (x がルート ノードの場合、pre[x] = x とします); 4. 検索操作では、次のセットを決定する必要があるとします。 x を特定し
、集合の代表要素を決定します。ルート ノードに到達するまで、pre[x] に沿ってツリー構造を上に移動できます。

したがって、このような特性を踏まえて、ユニオン検索の主な用途は次のとおりです。

  1. 無向グラフの接続性を維持します (2 つの点が同じ接続されたブロック内にあるかどうか、またはエッジの追加後にサイクルが生成されるかどうかを判断します)。
  2. 最小スパニング ツリーを解くための Kruskal のアルゴリズムで使用されます。

一般に、共用体検索は次の 3 つの操作に対応します。

  1. 初期化(Init()関数)
  2. 検索関数(Find()関数)
  3. マージ集計関数 (Join() 関数)

コードの概要を以下に示します。

#include <iostream>

using namespace std;

/*
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式
第一行包含两个整数 N,M,表示共有 N 个元素和 M 个操作。
接下来 M 行,每行包含三个整数 Z_i,X_i,Y_i。
当 Z_i=1时,将 X_i与 Y_i所在的集合合并。
当 Z_i=2时,输出 X_i与 Y_i是否在同一集合内,是的输出 Y ;否则输出 N 。

输出格式
对于每一个 Z_i=2的操作,都有一行输出,每行包含一个大写字母,为 Y 或者 N 。

输入输出样例
输入
4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4
输出
N
Y
N
Y
*/

const int maxn = 10001;
int pre[maxn]; //保存每个节点的前驱
int height[maxn]; //保存每个节点所在树的高度

void init(int n)
{
    //初始化操作
    for (int i = 0; i < n; i++)
    {
        pre[i] = i;
        height[i] = 1;
    }
}

int find(int x) //查找节点x所在树的根节点
{
    if (pre[x] == x)
        //约定根节点的前驱还是自己,所以x的父节点和自己相同时即可返回
        return x;
    return find(pre[x]); //小递归
    /*
    等同于:
    while(pre[x]!=x)
        x=pre[x]
    return x;
    */
}

int findpro(int x)
{
    //利用路径压缩进行优化
    if (pre[x] == x)
        return x;
    return pre[x] = findpro(pre[x]); //赋值语句的返回值为右值
}

void join(int x, int y)
{
    //查找x和y的根节点
    int fx = findpro(x), fy = findpro(y);
    //如果根节点不同,即处于不同的树,将fx的父节点设为fy,即将x所在子树加到y所在树的根节点下
    if (fx != fy)
        pre[fx] = fy;
}

void joinpro(int x, int y)//加权标记法
{
    x = find(x); //寻找 x的代表元
    y = find(y); //寻找 y的代表元
    if (x == y)  //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,直接返回;否则,执行下面的逻辑
        return;
    if (height[x] > height[y]) //如果 x的高度大于 y,则令 y的上级为 x
        pre[y] = x;
    else
    {
        if (height[x] == height[y]) //如果 x的高度和 y的高度相同,则令 y的高度加1
            height[y]++;
        pre[x] = y; //让 x的上级为 y
    }
}

int main()
{
    int n, m;
    cin >> n >> m;
    init(n);
    while (m--)
    {
        int z, x, y;
        cin >> z >> x >> y;
        if (z == 1)
            join(x, y);
        else
        {
            if (findpro(x) == findpro(y))
                cout << "Y" << endl;
            else
                cout << "N" << endl;
        }
    }
    return 0;
}

このブログは、以下のブログを基に簡略化し、私自身の理解を加えたものです。

[アルゴリズムとデータ構造] - Merge Search_the_ZED のブログ-CSDN Blog_ Merge Search

おすすめ

転載: blog.csdn.net/m0_51507437/article/details/122322566