Python の高度なアルゴリズムとデータ構造: コレクションの高速クエリとマージ

Python の高度なアルゴリズムとデータ構造: コレクションの高速クエリとマージ

コード設計では、このようなシナリオによく直面します。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 つの要素ペアのルート ノードをそれぞれ見つけ、高さが低いツリーのルート ノードを、高さが高いツリーの子ノードとして使用します。このプロセスは効率性にとって非常に重要です。これについては後でさらに詳しく説明します。ツリーのマージの状況を次の図に示します。

コードの実装を見てみましょう:

  1. # これはサンプルの Python スクリプトです
  2.  
  3. # ⌃Rを押す 実行する 自分のコード置き換えてください
  4. # 2 回押すと、クラス、ファイル、ツール ウィンドウ、アクション、設定などあらゆる場所を検索できます
  5.  
  6. クラス要素:
  7. def __init__(self, val: int ):
  8. 自己.val = val
  9. self.parent = self #要素は作成されると別のコレクションを形成するため、親ノードはそれ自身を指します
  10. 定義値(自己):
  11. self.valを返す
  12. 親(自分)を定義します。
  13. 自己.親を返す
  14. def set_parent(自分自身、親):
  15. 主張する なしではない
  16. 自己.親 = 親
  17.  
  18. クラスDisjointSet:
  19. __init__(self)を定義します。
  20. 自己.hash_map = {}
  21. def add (self, elem : 要素):
  22. 要素 なしではない
  23. self.hash_map内のelem.value() の場合:
  24. 戻る 間違い 
  25. self.hash_map[elem.value()] = elem
  26. 戻る 真実 
  27. def find_partition(self, elem: 要素):
  28. #要素が配置されているコレクションのルートノードを返します
  29. 要素  self.hash_map内のNoneまたはelem.value()ではない
  30. 親 = 要素.parent()
  31. if parent != elem: #ルートノードを再帰的に検索します。ツリーの高さは lg(n) なので、ここでの検索の時間計算量は lg(n) です。
  32. 親 = self.find_partition(親)
  33. を返す
  34.  
  35. def are_disjoint(self, elem1: 要素, elem2: 要素):
  36. # 2 つの要素が同じセットに属しているかどうかを判断するには、ハッシュ テーブルでそれらがマップされるルート ノードが同じかどうかを判断するだけです。
  37. ルート1 = 自己.パーティション検索(要素1)
  38. root2 = self.find_partition(要素2)
  39. ルート1返す ルートではない2
  40.  
  41. def merge(self, elem1: 要素, elem2: 要素):
  42. ルート1 = 自己.パーティション検索(要素1)
  43. root2 = self.find_partition(要素2)
  44. root1root2の場合:
  45. #2つの要素が同じセットに属している
  46. 戻る 間違い 
  47. ルート2.setParent(ルート1)
  48. self.hash_map[root2.value()] = root1 #root2に対応する親ノードを設定します
  49.  
  50. #スクリプトを実行するには、ガター緑色のボタンを押します。
  51. __name__ == '__main__'の場合:
  52.  
  53. # PyCharm のヘルプについては、 https://www.jetbrains.com/help/pycharm/参照してください。

コレクションの表現をキューからマルチツリーに変更したため、コレクションの検索とマージの対応する複雑さは O(lg(n)) です。ここで問題となるのは、効率をさらに向上できるかどうかです。現在のマージ関数は、親ポインタを経由してルートノードまで登らなければならないため、時間がかかります。親ポインタがルートノードを直接指すようにできれば、登る時間のオーバーヘッドを節約できるのではないでしょうか。次の図に示すように、下位ノードの親ポインタがルートノードを直接指すこの方法は、パス圧縮と呼ばれます。

上の図から、ノード 6 と 8 の親ノードは元々 9 であり、それが属するセットのルート ノードは 1 であることがわかります。したがって、元々 9 を指していたポインターをルート ノード 1 に直接ポイントすることで、将来セットをマージまたはクエリするときに上へ登る時間コストを節約できます。もう 1 つの問題は、上記のコードで 2 つのツリーをマージすることです。root2 の親ポインターを root1 にポイントするだけです。これにより、マージされたツリーのバランスが崩れます。つまり、マージ後の左と右のサブツリーの高さが大きく異なる可能性があり、次の図に示すように、これも効率に悪影響を及ぼします。

右下隅をマージした後、左右のサブツリーの高さの差が大きいため、ノード 6、8 がルート ノード 0 を見つけるのにかかる時間は、2、3、4 よりも長くなっていることがわかります。ただし、右上隅が形成されると、リーフ ノード 6、8 と 2、3、4 がルート ノードを見つけるのにかかる時間はほぼ同じになり、効率が向上します。したがって、ツリーの高さも記録する必要があります。マージするときは、高さの低いツリーを高さの高いツリーにマージする必要があります。したがって、コードは次のように変更されます。

  1. クラス要素:
  2. def __init__(self, val: int ):
  3. 自己.val = val
  4. self.parent = self #要素は作成されると別のコレクションを形成するため、親ノードはそれ自身を指します
  5. self.rank = 1 # 木の高さを示します
  6. 定義値(自己):
  7. self.valを返す
  8. 親(自分)を定義します。
  9. 自己.親を返す
  10. def set_parent(自分自身、親):
  11. 主張する なしではない
  12. 自己.親 = 親
  13. get_rank(self)を定義します。
  14. 自己ランクを返す
  15. def set_rank(自己、ランク):
  16. ランク > 1 を主張する
  17. 自己ランク = ランク

次にfind_partitionアプローチを修正する必要があります

  1. def find_partition(self, elem: 要素):
  2. #要素が配置されているコレクションのルートノードを返します
  3. 要素  self.hash_map内のNoneまたはelem.value()ではない
  4. 親 = 要素.parent()
  5. elemの場合: #すでにルートノード
  6. 要素を返す
  7. parent = self.find_partition(elem) #コレクションのルートノードを取得します
  8. elem.set_parent(parent) #パス圧縮はルートノードを直接指します
  9. return parent #ルートノードを返す

find_partion の実装には再帰的なプロセスがあることに注意してください。現在のノードがルート ノードでない場合は、ルート ノードが再帰的に照会され、現在のノードの親ポインタが直接ルート ノードを指します。この変更に必要な時間計算量は元の lg(n) と同じであることがわかります。

次に、マージの実装を変更する必要があります。

  1. def merge(self, elem1: 要素, elem2: 要素):
  2. ルート1 = 自己.パーティション検索(要素1)
  3. root2 = self.find_partition(要素2)
  4. root1root2 の場合: # 2 つの要素は同じセットに属します
  5. 戻る 間違い 
  6. 新しいランク = root1.get_rank() + root2.get_rank()
  7. if root1.get_rank() >= root2.get_rank(): # ツリーの高さに基づいてマージ方向を決定します
  8. ルート2.set_parent(ルート1)
  9. root1.set_rank(新しいランク)
  10. それ以外
  11. ルート1.set_parent(ルート2)
  12. root2.set_rank(新しいランク)
  13. 戻る 真実 

この改善後、find_partion と merge の m 回の呼び出しに必要な時間は O(m) です。つまり、改善後、find_partion と merge が多数回呼び出された場合、これらの呼び出しの平均時間は O(1) に短縮されます。つまり、パス圧縮後、大量の検索セット操作とマージセット操作を呼び出す際の効率が大幅に向上します。対応する数学的証明は非常に詳細なので、今は無視します。ここでの効率向上は実感できないかもしれませんが、考えてみてください。WeChatでは、2人が同じグループに属しているかどうかを判断するための通話が1日に少なくとも数千万回、場合によっては数億回行われるため、ここでの改善によりサーバーの処理効率が大幅に向上します。

完全なコードはここにあります

https://github.com/wycl16514/python_disjoint_set.git

<<:  クラウドコンピューティングと人工知能が、先進的な企業に前例のない機会を生み出す方法

>>:  メタバースがますます熱を帯びる中、開発者はどのような主要テクノロジーを掘り下げていくべきでしょうか?

ブログ    

推薦する

未来の世界:すべてが AI になる?

人工知能は私たちの日常生活にますます大きな影響を与えており、研究分野に革命をもたらしています。 ET...

人工知能は非常に強力だが、人間は必ずしも人工知能に支配されるわけではない。ホーキングは間違っているのだろうか?

著者: ふす有名な物理学者ホーキング博士はかつて、将来人類は人工知能によって滅ぼされるかもしれないの...

パンデミックの間、AI はどのようにして工場の安全な運営を維持するのに役立つのでしょうか?

[[377897]]新型コロナウイルスの感染拡大が世界中で続く中、多くの企業が業務をリモートワーク...

インドの農業変革における人工知能の役割

農業はインドの人口の約58%の生計を支えています。漁業、林業、農業の総付加価値は2020年度で194...

畳み込みニューラルネットワークの「修理工」のための「マニュアル」

[[242145]]ビッグデータダイジェスト制作編集者: Luo Ran、Xue Qing、Ail...

C#アルゴリズムで解決した面接の質問

C# アルゴリズムはプログラマーの面接でよく出題されますが、C# アルゴリズムを使用してそれをどのよ...

AI が「インターネット セレブ」メタバースと出会うと、どのような変化が起こるでしょうか?

2021年に世界を爆発的に盛り上げたメタバースは、2022年に入っても冷める気配がなく、今も多くの...

食習慣の変化に伴い、スマートロボットキッチン技術が熱を帯びる

COVID-19パンデミックが続く中、非接触型の食事がますます人気になっています。宅配やテイクアウト...

2019 年の AI 統計と重要な事実

[[280183]] [51CTO.com クイック翻訳] 人工知能(AI)は日々驚異的なスピードで...

機械学習がサイバー脅威に対する最善の武器である理由

攻撃対象領域が拡大し続け、攻撃手法がより高度化するにつれ、セキュリティ業界は現在、深刻な「セキュリテ...

...

機械学習の謎を解く: プログラムはどのようにして自分自身を作成するのか?

この記事は公開アカウント「Reading Core Technique」(ID: AI_Discov...

ディープラーニングの父、ヒントン:次世代ニューラルネットワーク

ディープラーニングの父ヒントン氏:次世代ニューラルネットワーク SIGIRは、情報検索の分野における...

...