コード設計では、このようなシナリオによく直面します。2 つの要素が与えられた場合、それらが同じセットに属しているかどうかをすばやく判断する必要があります。同時に、必要に応じて、異なるセットを 1 つのセットにすばやくマージできます。たとえば、ソーシャル アプリケーションを開発する場合、ここで説明した関数を使用して、2 人のユーザーが友人であるかどうか、または同じグループに属しているかどうかを判断する必要があります。 これらの関数は単純に思えるかもしれませんが、難しいのは「十分に速く」処理することです。2 つの要素 a と b がそれぞれセット A と B に属しているとします。これらが同じセットに属しているかどうかを判断する直接的な方法は、セット A のすべての要素を走査して b が見つかるかどうかを確認することです。セット A に n 個の要素が含まれている場合、この方法の時間計算量は O(n) です。セットに多くの要素があり、判断の数も大きい場合、この方法は非常に非効率的になります。このセクションでは、サブ線形アルゴリズムが見つかるかどうかを確認します。 まず、複雑度が O(n) のアルゴリズム ロジックを見てみましょう。0 から 6 までの番号が付けられた 6 つの要素があるとします。キューを使用してセットをシミュレートできます。同じセットに属する要素は同じキューに格納され、次の図に示すように、各要素はハッシュ テーブルを通じてキューの先頭にマッピングされます。 このデータ構造では、2 つの要素が同じセットに属しているかどうかを確認するには、ハッシュ テーブルを通じてそれぞれの要素が配置されているキューの先頭を見つけ、先頭が一貫しているかどうかを判断するだけです。2 つの要素が同じセットに属しているかどうかを示すには、areDisjoint(x,y) を使用します。現在のデータ構造では、areDisjoint の時間計算量は O(1) です。 2 つの要素のセットを結合する場合は、merge(x,y) を使用して表現します。次に、現在の構造では、次の図に示すように、x と y に対応するキューの先頭を見つけ、x が配置されているキューの先頭から最後の要素までトラバースし、最後の要素の次のポインターを y が配置されているキューの先頭に移動するだけです。 同時に、2 番目のセットの各要素によってマップされるキュー ヘッドを変更する操作も実行する必要があります。したがって、現在の構造では、merge(x,y) の時間計算量は O(n) です。キュー ヘッドから末尾までのトラバースは O(n) であり、y が配置されているセットの各要素をトラバースして、それらがマップするキュー ヘッドを変更するのにも O(n) の時間計算量があるためです。 ここで問題となるのは、マージに必要な時間を最適化できるかどうかです。マージ時に時間のかかるステップが 2 つあることに気付きました。1 つはキューからキューの末尾まで移動すること、もう 1 つは 2 番目のセットの各要素が指すキューの先頭を変更することです。つまり、時間の消費は、コレクションを表すためにキューを使用するという事実によって実際に発生します。時間を最適化するために、次の図に示すように、キューをマルチブランチ ツリーに置き換えます。 この時点で、要素をキューの先頭にマッピングするためにハッシュ テーブルは使用しなくなりました。代わりに、同じセットの要素を同じマルチブランチ ツリーに挿入します。2 つの要素が同じセットに属しているかどうかを判断するには、ツリーのルート ノードが見つかるまで、要素の親ノード ポインターをたどるだけです。同じルート ノードが見つかった場合、2 つの要素は同じセットに属します。ソートされたバイナリ ツリーの場合、ツリーの高さは O(lg(n)) です (n はツリー内のノードの数)。したがって、2 つの要素が同じセットに属しているかどうかを判断するために必要な時間の計算量は O(lg(n)) です。 2 セットの要素をマージする必要がある場合、2 つの要素ペアのルート ノードをそれぞれ見つけ、高さが低いツリーのルート ノードを、高さが高いツリーの子ノードとして使用します。このプロセスは効率性にとって非常に重要です。これについては後でさらに詳しく説明します。ツリーのマージの状況を次の図に示します。 コードの実装を見てみましょう:
コレクションの表現をキューからマルチツリーに変更したため、コレクションの検索とマージの対応する複雑さは O(lg(n)) です。ここで問題となるのは、効率をさらに向上できるかどうかです。現在のマージ関数は、親ポインタを経由してルートノードまで登らなければならないため、時間がかかります。親ポインタがルートノードを直接指すようにできれば、登る時間のオーバーヘッドを節約できるのではないでしょうか。次の図に示すように、下位ノードの親ポインタがルートノードを直接指すこの方法は、パス圧縮と呼ばれます。 上の図から、ノード 6 と 8 の親ノードは元々 9 であり、それが属するセットのルート ノードは 1 であることがわかります。したがって、元々 9 を指していたポインターをルート ノード 1 に直接ポイントすることで、将来セットをマージまたはクエリするときに上へ登る時間コストを節約できます。もう 1 つの問題は、上記のコードで 2 つのツリーをマージすることです。root2 の親ポインターを root1 にポイントするだけです。これにより、マージされたツリーのバランスが崩れます。つまり、マージ後の左と右のサブツリーの高さが大きく異なる可能性があり、次の図に示すように、これも効率に悪影響を及ぼします。 右下隅をマージした後、左右のサブツリーの高さの差が大きいため、ノード 6、8 がルート ノード 0 を見つけるのにかかる時間は、2、3、4 よりも長くなっていることがわかります。ただし、右上隅が形成されると、リーフ ノード 6、8 と 2、3、4 がルート ノードを見つけるのにかかる時間はほぼ同じになり、効率が向上します。したがって、ツリーの高さも記録する必要があります。マージするときは、高さの低いツリーを高さの高いツリーにマージする必要があります。したがって、コードは次のように変更されます。
次にfind_partitionアプローチを修正する必要があります
find_partion の実装には再帰的なプロセスがあることに注意してください。現在のノードがルート ノードでない場合は、ルート ノードが再帰的に照会され、現在のノードの親ポインタが直接ルート ノードを指します。この変更に必要な時間計算量は元の lg(n) と同じであることがわかります。 次に、マージの実装を変更する必要があります。
この改善後、find_partion と merge の m 回の呼び出しに必要な時間は O(m) です。つまり、改善後、find_partion と merge が多数回呼び出された場合、これらの呼び出しの平均時間は O(1) に短縮されます。つまり、パス圧縮後、大量の検索セット操作とマージセット操作を呼び出す際の効率が大幅に向上します。対応する数学的証明は非常に詳細なので、今は無視します。ここでの効率向上は実感できないかもしれませんが、考えてみてください。WeChatでは、2人が同じグループに属しているかどうかを判断するための通話が1日に少なくとも数千万回、場合によっては数億回行われるため、ここでの改善によりサーバーの処理効率が大幅に向上します。 完全なコードはここにあります https://github.com/wycl16514/python_disjoint_set.git |
<<: クラウドコンピューティングと人工知能が、先進的な企業に前例のない機会を生み出す方法
>>: メタバースがますます熱を帯びる中、開発者はどのような主要テクノロジーを掘り下げていくべきでしょうか?
7月4日に開催された百度AI開発者会議(Baidu Create 2018)で、百度上級副社長兼AI...
この記事は公開アカウント「Reading Core Technique」(ID: AI_Discov...
2021年、研究者たちは一連のマイクロモデルを訓練しているときに驚くべき発見をしました。それは、長期...
海外メディアの報道によると、マイクロソフトは水曜日、小規模な人工知能スタートアップ企業であるボンサイ...
[[403843]]画像ソース: https://pixabay.com/images/id-59...
近年、デジタル変革の波に牽引され、自動車業界は着実な変革、アップグレード、ビジネスの再編を遂げていま...
2021 年が始まりました。過去 1 年間で機械学習コミュニティでは多くの出来事がありました。時間...
8月3日(東部時間8月2日)、Metaは、ユーザーがテキストプロンプトを通じて音楽やオーディオを作...
著名なIT技術メディア51CTOが主催する第16回中国企業年次選考活動「IT印象:イノベーションの活...
紙: http://wanghao.in/paper/NatureMedicine21_MSA.pd...