Leetcode質問クラス丨2020年の最新のデータ構造とアルゴリズムの紹介、Zuo Chengyun氏とMa Yingjun氏がDachangインタビュアーにご案内します
アルゴリズムフレームワーク
質問について考えてみましょう。強連結成分の分解アルゴリズムのコア原則は何ですか?
以前の記事を読んだことがあれば、この質問に答えることは難しくありません。これは強く接続されたコンポーネントであるため、コンポーネント内のすべてのポイントを相互に接続できることを意味します。したがって、あるポイントから開始し、ループを見つけてそれを開始ポイントに戻すことができると簡単に考えることができます。このようにして、通過する点はすべて、強連結成分の一部です。
ただし、この方法には問題があります。つまり、強連結成分のすべての点を通過し、漏れがないことを確認する必要があります。この問題の解決策を考えることもできます。たとえば、検索アルゴリズムを使用して、到達可能なすべてのポイントとすべてのパスを検索できます。しかし、このように、私たちは別の問題に遭遇します。この問題は、強く接続されたコンポーネント間の接続の問題です。
例を見てみましょう:
上の画像では、ポイント1から開始すると、画像のすべてのポイントに到達できます。しかし、1、2、3は強連結成分であり、4、5、6は別の成分であることがわかります。1が配置されている強連結成分を探すと、4、5、6の3つの点も含まれる可能性があります。しかし、問題はそれらが自己構成要素であり、1の強く関連する構成要素に数えられるべきではないということです。
上記の分析とアイデアを整理すると、強連結成分分解アルゴリズムのコアは、実際には完全性問題であるこれら2つの問題を解決することであることがわかります。完全性とは、漏れや冗長性、エラーがないことを意味します。核となる問題を理解した後、思考フレームワークを構築するのは簡単です。次に、アルゴリズムの説明を見て、理解がはるかに容易になります。
アルゴリズムの詳細
Tarjanのアルゴリズムの最初のメカニズムはtimestampです。つまり、通過するときに、通過した各ポイントに値がマークされます。この値は、どの要素がトラバースされるかを示します。
これは理解しやすいはずです。グローバル変数を維持するだけでよく、トラバースするときにグローバル変数を増加させることができます。みんながデモできるようにPythonコードを書き留めましょう。
stamp = 0
stamp_dict = {}
def dfs(u):
stamp_dict[u] = stamp
stamp += 1
for v in Graph[u]:
dfs(v)
复制代码
タイムスタンプにより、各ポイントが訪問される順序を知ることができます。この順序は順方向です。たとえば、uとvが2つのポイントであり、uのタイムスタンプがvより小さいと仮定します。次に、それらの間の関係には2つの可能性しかありません。1つ目は、uをvに接続できることです。これは、uからvへのリンクを接続できることを示します。2つ目は、uをvに接続できないことです。この場合、vからuへの逆接続は、相互に接続してはならないため、意味がありません。
したがって、接続されたパスを見つけるには、リバースパスを見つける必要があります。コサラジュアルゴリズムでは、これを実現するためにリバースグラフを使用します。タルジャンでは、別のアプローチが取られました。各ポイントのタイムスタンプはすでにわかっているため、タイムスタンプを介して逆のパスを見つけることができます。どういう意味ですか?実際、それは非常に単純です。uをトラバースするときに、uより小さいvに遭遇した場合、uからvへの逆パスがあります。この時点でvがスタックからポップされていない場合、つまりvがuの上流にある場合は、vからuへのパスがあることも意味します。これは、uとvを相互に接続できることを示しています。
相互に接続されたuとvのペアが見つかったので、それらを記録する必要があります。しかし問題は、録音がいつ終了するかをどのように知るかです。この境界はどこですか?Tarjanアルゴリズムは、この問題を解決するために別の独創的なメカニズムを設計しました。
このメカニズムは低メカニズムであり、low [u]は、ポイントuが接続できるすべてのポイントのタイムスタンプの最小値を表します。タイムスタンプが小さいほど、検索ツリーの上位になり、uが接続できる検索ツリーの最高点として理解することもできます。次に、この点が、点uの強連結成分が配置されている検索ツリーのサブツリーのルートであることは明らかです。
ここに少しひねりがあるかもしれません、別の写真を見てみましょう:
グラフ内のノードのシーケンス番号は、再帰的トラバーサルのタイムスタンプです。グラフの各ポイントで、それらの低い値が1であることがわかります。明らかに、ポイント1は、検索ツリーの3つのポイント2、3、および4の祖先です。つまり、この強く接続されたコンポーネントのトラバーサルは、ポイント1から始まります。ポイント1がスタックからポップされると、1ビットツリーのルートを持つサブツリーがトラバースされ、すべての可能な強い接続コンポーネントが見つかったことを意味します。
これは別の問題を引き起こし、現在のポイントをuと仮定します。ポイントuが図の1のようなツリールートであるかどうかをどうやって知るのでしょうか。それをマークする方法はありますか?
もちろん、そのようなポイントの特徴の1つは、タイムスタンプがローと同じであることです。したがって、配列を使用して、見つかった強連結成分を維持し、これらの強連結成分をたどることができるツリーのルートがスタックの外にある場合は、配列を空にすることができます。
上記のロジックを完了した後、コードを記述できます。
scc = []
stack = []
def tarjan(u):
dfn[u], low[u] = stamp, stamp
stamp += 1
stack.append(u)
for v in Graph[u]:
if not dfn[v]:
tarjan(v)
low[u] = min(low[u], low[v])
elif v in stack:
low[u] = min(low[u], dfn[v])
if dfn[u] == low[u]:
cur = []
# 栈中u之后的元素是一个完整的强连通分量
while True:
cur.append(stack[-1])
stack.pop()
if cur[-1] == u:
break
scc.append(cur)
复制代码
最後に、前に述べた典型的な例を見てみましょう。
まず、1から始めて6の終わりまで深く検索を続けます。6にトラバースすると、DFN [6] = 4、low [6] = 4、6がスタックからポップされると条件が満たされ、6は独立して強力な接続と呼ばれます重量。
同様に、5が終了したときにも条件が満たされ、2番目の強連結成分が得られます。
次に、ノード3に戻ります。ノード3もノード4に移動でき、4は1に接続できます。1ポイントはすでにスタック内にあるため、1ポイントの再帰は続行されず、low [4] = 1のみが更新され、4が終了すると、3が再度更新され、low [3] = 1になります。
最後にノード1に戻り、ノード1をノード2に移動します。2に接続できる4つのポイントは既にスタックにあり、DFN [4]> DFN [2]なので、2つのポイントは更新されません。再び1時の位置に戻った後、1時の位置に他のポイントを接続することはできません。終了します。終了時に、low [1] = DFN [1]であることがわかりました。現時点では、スタック内の残りの4つの要素はすべて強く接続されたコンポーネントです。
これでアルゴリズムフロー全体の紹介は終わりましたので、今日のコンテンツをお楽しみいただければ幸いです。
私はChengzhiです。皆さんの毎日の利益を心から願っています。それでも今日のコンテンツが好きな場合は、3つのリンクのサポートに来てください(例:、フォロー、転送)。
著者:Chengzhi
リンク:https://juejin.im/post/6875498612537851918