この記事は個人のパブリックアカウントから作成されました:TechFlow、オリジナルは簡単ではありません、注意を求めてください
今日は、アルゴリズムとデータ構造で15番目、動的プログラミングシリーズで4番目です。
以前の記事でバックパックの問題について話していましたが、少し疲れているかどうかはわかりません。クラシック記事には9つのバックパックがありますが、競技者に加えて、単調な最適化はすでに非常に優れていることがわかります。依存関係のあるバックパックの問題やハイブリッドバックパックの問題と同様に、一部の剣が斜めになるため、ここではあまり触れません。興味があれば、Baidu Backpack 9を自分で確認できます今日は興味深い質問を見てみましょうこの興味深い質問を通して、ツリー構造での動的プログラミングの方法を見てみましょう。
この質問の意味は非常に単純です。ツリーが与えられた場合、それは必ずしもバイナリツリーではありません。ツリーのブランチは重み付けされ、長さと見なすことができます。ツリーの最長リンクの長さはどれくらいですか?
たとえば、手元にある木を描く場合、それは醜いかもしれませんが、非難しないでください。
肉眼でそれを見て少し答えを見つけようとすると、最長経路は下の図の赤い経路になります。
しかし、アルゴリズムを機能させたらどうなるでしょうか。
この問題を解決するには、実際には非常に巧妙な方法があります。最初にそれについて話をしましょう。動的プログラミングがこの問題を解決する方法を見てみましょう。
ツリーDP
動的プログラミングは、配列で機能するだけでなく、データ構造に関係なく、状態遷移の条件を満たし、動的プログラミングの余効がない限り、実際に使用できます。同じことがツリーにも当てはまります。これを理解すると、あと2つの質問しか残っていません。最初の質問は状態とは何か、2番目の質問はどのように状態間を転送するかです。
以前のバックパックの問題では、状態はバックパックの現在のボリュームであり、転送は新しいアイテムを取るという私たちの決定です。しかし、今回はツリーで動的プログラミングを行う必要があります。比較的言えば、状態と対応する遷移は非表示になります。それは問題ではありません。最初からアイデアを整理し、派生と思考のプロセスを少しずつ説明します。
まず第一に、私たちはすべての状態間の遷移が本質的にローカルによって全体を計算するプロセスであることを知っています。比較的簡単なサブステートを移動して、全体的な結果を取得します。これが動的計画法の本質であり、ある程度は分割統治法にも近く、大きな問題と小さな問題の間には論理的な関係があります。したがって、大きな問題に直面した場合、分割統治法から学び、小さな問題から始めることを考えることができます。
したがって、小規模から大規模、マイクロからマクロまでの最も単純な状況を見てみましょう。
この場合、リンクが1つしかないことが明らかであるため、長さは当然5 + 6 = 11であり、これも明らかに最長の長さです。この場合は問題ありません。状況をもう少し複雑にしてみましょう。ツリーにレイヤーをもう1つ追加しましょう。
この図はもう少し複雑ですが、パスを見つけるのは難しくありません。EBFHである必要があります。パスの全長は12です。
しかし、パスの長さを変更すると、たとえばFGとFHのパスを長くすると、どのような結果になりますか?
この場合は明らかに答えが変わりますが、FGHが最も長くなります。
この例は、ツリーの場合、その上の最長パスがルートノードを通過するとは限らないという非常に単純な問題を説明するためのものです。たとえば、前の例では、パスがBを通過する必要がある場合、最長の長さは4 + 2 + 16 = 22としてのみ構成できますが、Bを通過できない場合、最長の長さは31になります。
この結論を引き出すのは役に立たないように見えるかもしれませんが、実際には、アイデアを整理するのに非常に役立ちます。最長パスが必ずツリーのルートを通過することは保証できないため、回答を直接転送することはできません。どうする?
この質問に答えるだけでは不十分であり、私たちは依然として観察し、深く考える必要があります。
転送プロセス
次の2つの画像を見てみましょう。
パターンは見つかりましたか?
データ構造はツリー型であるため、接続する2つのノードに関係なく、この最長パスはサブツリーのルートノードを通過することが保証されます。この謙虚な結論を過小評価しないでください。実際、それは非常に重要です。この結論で、ルートノードでパス全体をカットしました。
分割後、リーフノードへのリンクが2つありましたが、問題は、ルートノードからリーフノードへのリンクがたくさんあることです。
これらの2つのリンクが最も長いので、それは単純です。したがって、この方法で加算すると、最長のリンクが保証されます。これらの2つのリンクはリーフノードからAへのリンクであるため、取得される最長のリンクは、Aをルートノードとするサブツリーの最長パスです。
以前の分析では、最長パスは転送できませんが、葉までの最長距離は転送できます。例を見てみましょう:
Fと葉の間の最長距離は明らかに5と6の大きい方ですが、Bはもう少し複雑で、DとEはどちらも葉ノードであり、理解しやすくなっています。また、Fのリーフノードではない子ノードFも持っていますが、Fからリーフノードまでの最長距離は6であるため、Bからリーフノードまでの最長距離は2 + 6 = 8。このようにして、状態遷移方程式を取得しますが、転送するのは必要な答えではなく、現在のノードからリーフノードまでの最長距離と2番目に長い距離です。
最長距離だけでは十分ではないため、ルートノードの最長パスを取得するには、ルートノードの最長距離を最長パスに追加する必要があるため、前述のとおり、すべてのパスはサブツリーのルートノードを通過する必要があります。 。これは理解するのはナンセンスですが、この状態は確かに非常に重要です。すべてのリンクが少なくとも1つのサブツリーのルートノードを通過するため、ルートノードを通過するすべてのサブツリーの最長パスを計算します。最長のパスが答えではありませんか?
以下に、このプロセスを示します。
転送プロセスは、上の図ではピンクのペンでマークされています。リーフノードの場合、最長距離と2番目に長い距離はどちらも0であり、メイン転送プロセスは中間ノードで発生します。
遷移のプロセスも簡単に理解できます。中間ノードiの場合、そのすべての子ノードjをトラバースし、最大値と2番目に大きい値を維持して、状態転送方程式を記述します。
状態遷移を理解したいのですが、残りはコーディングの問題です。特に再帰的な場合は、ツリーで状態遷移を行うのは直観に反するかもしれませんが、実際には難しくありません。コードを書いて見てみましょう。まず、ツリーのこの部分を見てみましょう。操作を簡単にするために、ツリー内のすべてのノード番号をintと見なすことができます。各ノードには、親ノードを含むこのノードに接続されているすべてのエッジを格納する配列があります。
ツリー上のリンクの長さにのみ注意し、ツリーの構造は気にしないので、ツリーが構築された後、ツリーのルートの結果は、どのポイントが全体であるかに関係なく同じです。したがって、ツリー全体のルートノードとしてノードを見つけ、再帰します。強調すると、これは非常に重要なプロパティです。本質的に、ツリーは無向無循環完全接続グラフであるためです。したがって、どのノードがルートノードであっても、サブツリー全体を接続できます。
idと2つの最長および2番目に長い長さを含むノード情報を格納するクラスを作成します。コードを見てみましょう。コードは思ったよりずっと単純なはずです。
class Node(object):
def __init__(self, id):
self.id = id
# 以当前节点为根节点的子树到叶子节点的最长链路
self.max1 = 0
# 到叶子节点的次长链路
self.max2 = 0
# 与当前节点相连的边
self.edges = []
# 添加新边
def add_edge(self, v, l):
self.edges.append((v, l))
# 创建数组,存储所有的节点
nodes = [Node(id) for id in range(12)]
edges = [(0, 1, 3), (0, 2, 1), (1, 3, 1), (1, 4, 4), \
(1, 5, 2), (5, 6, 5), (5, 7, 6), (2, 8, 7), (7, 9, 2), (7, 10, 8)]
# 创建边
for edge in edges:
u, v, l = edge
nodes[u].add_edge(v, l)
nodes[v].add_edge(u, l)
私たちはアイデアを伝えることだけを目的としているため、オブジェクト指向コードの多くは省略されていますが、問題のアイデアを理解するにはそれで十分です。
以下に、ツリーの動的プログラミングのコードを示します。
def dfs(u, f, ans):
nodeu = nodes[u]
# 遍历节点u所有的边
for edge in nodes[u].edges:
v, l = edge
# 注意,这其中包括了父节点的边
# 所以我们要判断v是不是父节点的id
if v == f:
continue
# 递归,更新答案
ans = max(ans, dfs(v, u, ans))
nodev = nodes[v]
# 转移最大值和次大值
if nodev.max1 + l > nodeu.max1:
nodeu.max2 = nodeu.maxi1
nodeu.max1 = nodev.max1 + l
elif nodev.max1 + l > nodeu.max2:
nodeu.max2 = nodev.max1 + l
# 返回当前最优解
return max(ans, nodeu.max1 + nodeu.max2)
非常に複雑なツリー型のDPのように見えますが、実際にはコードは数十行しかありませんが、驚くほど簡単なのでしょうか。
これらの数十行のコードは単純に見えますが、特に再帰的な操作に関しては、詳細がまだいくつかあります。特に再帰に慣れていない学生にとっては難しいかもしれませんが、前の図を参考に手作業で紙で確認することをお勧めしますが、理解は深まると思います。
別のアプローチ
記事はまだ終わっていません。まだ小さな卵があります。実際、この質問には別の方法がありますが、この方法は非常に賢く、すべての人に紹介されています。
以前に、ツリーはノードの接続状態を記録するため、どのノードがルートノードであるかに関係なく、ツリー全体のパスの長さと構造には影響しません。この場合、想像力があれば、木をつぶすことができます。それは、紐または木の棒がつながっていると見なすことができますか?
下の写真を見てみましょう:
我々は持っているより近いCを指すように点Bに、そして木の構造には影響しません、すべての後に、これは抽象化されたアーキテクチャである、私たちは木の枝の間の角度を心配されていません。点Aを拾い上げ、他の点は重力によってたわみ、最終的に直線に引き込まれると想像できます。
たとえば、上の図では、ポイントAを取得し、BCDを停止しました。このとき、最低点はD点です。次に、ポイントDを選択すると、最下部のポイントがポイントCになり、DC間の距離がツリー上の最長リンクになります。
プロセス全体を整理し、最初にランダムにポイントをツリーのルートとして選択し、そこから最も遠いポイントを見つけました。2回目は、この最遠点をツリーのルートとして選択し、最遠点を再び見つけます。これらの2つの最も遠い点の間の距離が答えです。
このアプローチは非常に直感的ですが、厳密に証明できる方法を考えることはできません。わからない場合は、数本のロープでつなげて、一緒に運んで実験してみましょう。この方法で取得された2つのポイントが、ツリーで最も遠い2つのポイントであるかどうかを確認します。
最後に、コードを見てみましょう。
def dfs(u, f, dis, max_dis, nd):
nodeu = nodes[u]
for edge in nodes[u].edges:
v, l = edge
if v == f:
continue
nodev = nodes[v]
# 更新最大距离,以及最大距离的点
if dis + l > max_dis:
max_dis, nd = dis+l, nodev
# 递归
_max, _nd = dfs(v, u, dis+l, max_dis, nd)
# 如果递归得到的距离更大,则更新
if _max > max_dis:
max_dis, nd = _max, _nd
# 返回
return max_dis, nd
# 第一次递归,获取距离最大的节点
_, nd = dfs(0, -1, 0, 0, None)
# 第二次递归,获取最大距离
dis, _ = dfs(nd.id, -1, 0, 0, None)
print(dis)
この時点で、この興味深いトピックが終了しても、記事で2つの方法を学んだことを知っていますか?初めて少し混乱するように見えるかもしれませんが、多くの問題があるのは普通ですが、基本原則は難しくありません。絵を描いて適切な計算を行うと、間違いなく正しい結果を得ることができます。
今日の記事はそれだけです。もし何かやりがいを感じた場合は、フォローまたは再投稿してください。あなたの努力は私にとって非常に重要です。