-
Notifications
You must be signed in to change notification settings - Fork 0
LeetCode 310. Minimum Height Trees #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5bd5bdf
fa333ea
8f2a311
d6057ad
2fa1073
5918baa
c074362
25e4fd3
41bc230
9a652f2
14cb837
4be9eff
1701a90
4c732a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Step 1 | ||
|
|
||
| 素直に全ての root を試す方法しか思いつかなかった -> `step1_tle.py`。 | ||
|
|
||
| 後から思ったが、cycle がないことが保証されているので、一つ前に戻るのだけスキップする形の方がシンプルだった。 | ||
|
|
||
| ```py | ||
| for adj in node_to_adjacents[node]: | ||
| if adj == parent: | ||
| continue | ||
| child_height = max(child_height, 1 + height(adj, node)) | ||
| return child_height | ||
| ``` | ||
|
|
||
| 制約が | ||
|
|
||
| > `1 <= n <= 2 * 10^4` | ||
|
|
||
| で、全ての n から探索を行うと `(2 * 10^4) * (2 * 10^4) = 4 * 10^8`。 | ||
| C++ が 1 秒間に `10^8 ~ 10^9` ステップの処理を行えるとすると、Pythonがそれより大体 100 倍遅いから 1 秒間で `10^6 ~ 10^7` ステップ。 | ||
| なので、大体 `10 ~ 100` 秒くらいの実行時間になりそう。これは LeetCode 上でギリギリ Time Limit Exceeded になるかならないかくらいなので、微妙。案の定、TLEになった。 | ||
|
|
||
| そのまま実装するとTLEになりそうなのはわかっていたので、何かしらのヒューリスティックが思いつかないか、考えてみて、一つ思い浮かんだのは、枝が一番多いノードを root として選ぶと木の高さが最小になるのでは、と思ったが、幾つか例を試してみると、うまくいかないことがわかった。 | ||
|
|
||
| ``` | ||
| 5 | ||
| | | ||
| 0 - 1 - 2 - 3 - 4 | ||
| | | ||
| 6 | ||
| ``` | ||
|
|
||
| 上の例だと、2 が本当の最小の高さを作る root。 | ||
|
|
||
| LeetCode 上の Hint を考えてみる。 | ||
|
|
||
| > How many MHTs can a graph have at most? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 私はこのヒントの意図が全然わからなくて、相当強引に、一度てきとうなLeafをRootにして高さを数えさせてから、一つずつRootにするNodeをずらしていくとどうなるか、という方法で最初計算させました。(結果、そこまで遅くはないものの、60行ぐらいのとんでもないコードができてしまった...) |
||
|
|
||
| 幾つか例を考えてみると、雑に考えてグラフの端から端までいく最長のパスかあって、それの真ん中のノードが 1 つか 2 つあり、それが Minimum Height Tree を形成しそう? | ||
| 結構時間を使ってしまったので、悔しいが、解法を見てしまうことにする。 | ||
|
|
||
| Discord 上で取り組まれた方はいなさそうなので、LeetCode の Solutions を眺めてみると、これらの記事が理解しやすそう | ||
|
|
||
| [dietpepsi - Share some thoughts](https://leetcode.com/problems/minimum-height-trees/solutions/76055/share-some-thoughts-by-dietpepsi-mjsc/) | ||
| [lc\_1000xCoder - [ Full Explanation ] BFS - Remove Leaf Nodes](https://leetcode.com/problems/minimum-height-trees/solutions/5060930/full-explanation-bfs-remove-leaf-nodes-b-4x00/) | ||
|
|
||
| まず、Minimum Height Tree を作る root は 1 つか 2 つという私の考えは正しかった。それをどう求めるのかだが、各ステップで現在の葉ノードを全て削除していって、最後に残った 1 個か 2 個のノードがグラフの一番長いパスの真ん中 -> Minimum Height Tree の root(s) になる。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この1つか2つになる、というのを直接証明するのは少しむずかしいかもと思ったりしたのですが、Treeから全てのLeafを取り去る操作を繰り返し行う場合を考えるとき、仮に3つ以上Nodeがある場合、いずれかのNodeは必ず2本以上のEdgeを持つことになり(そうでないとTree全体がつながることがない)、Leafを取り去る操作を繰り返すと必ず1つか2のNodeが残るのですね。このLeafを一式取り去る行為の回数が、heightになるわけですね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. そうか、そうすると連結成分の個数が2以上の場合はどうしますか?という追加の問いかけはできますね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このやり方はエレガントですが、もう少し愚直には、無向木の直径を求めて、その経路の中間と考えると素直ではないですか。 木の上でできるだけ長い経路(直径)を求めるのはできますね。ちょっとトリッキーな方法として、DFS 2回で行うというのがあります。適当な点から最も遠い点を計算し、そこから最も遠い点を計算すると、直径の両端になります。両端が求まったらその最短経路を計算して、中間を取ります。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. たしかにそうですね。半分になる中間点を見つける、というのがヒントで言いたいことだったのかもしれないと思いました。 これはお遊びですが、Geminiに3つの方法で計算する様子を可視化してもらいました。
https://sasanquaneuf.github.io/math-toybox/apps/cs_mht.html |
||
|
|
||
| やり方だけ頭において、LeetCode 207\. Course Schedule を思い出しながら書いた `step1.py`。 | ||
|
|
||
| 時間計算量: `O(n)`、隣接リストの構築 `O(n)` + 各ノードは高々一回だけ葉として処理される `O(n)` + 各ノードの隣接リストは高々1回しか走査されないため (`adjacents[leaf]`) 合計で `O(n)` | ||
| 空間計算量: `O(n)`、neighbors が合計で `2(n - 1)`、degrees/leaves/next leaves も `O(n)`。 | ||
|
|
||
| # Step 2 | ||
|
|
||
| 上のポストを見返すと、`neighbors: list[set[int]]` を持ってそれを 隣接ノードの取得 + 長さによる枝の本数チェック、の両方に使用すれば、`neighbors` と `degrees` を別々に持たなくても済んでいる。 | ||
| 時間・空間計算量が変わることはなく、隣接ノードと持っている枝の本数を一つの変数にまとめることが読みやすさにどれだけ影響があるのか判断がつかないが、とりあえず書いてみることにする -> `step2.py`。 | ||
|
|
||
| # Step 4 | ||
|
|
||
| [Stack Overflow - Proof of correctness: Algorithm for diameter of a tree in graph theory](https://beta.stackoverflow.com/questions/20010472/proof-of-correctness-algorithm-for-diameter-of-a-tree-in-graph-theory) | ||
|
|
||
| Performing BFS (or DFS) twice to find the farthest node in a tree yields the diameter. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| class Solution: | ||
| def findMinHeightTrees(self, n: int, edges: list[list[int]]) -> list[int]: | ||
| if n == 1: | ||
| return [0] | ||
|
|
||
| neighbors = [[] for _ in range(n)] | ||
| degrees = [0] * n | ||
|
|
||
| for node1, node2 in edges: | ||
| neighbors[node1].append(node2) | ||
| neighbors[node2].append(node1) | ||
| degrees[node1] += 1 | ||
| degrees[node2] += 1 | ||
|
|
||
| leaves = [node for node in range(n) if degrees[node] == 1] | ||
| remaining = n | ||
| while remaining > 2: | ||
| next_leaves = [] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 取り去る解法でコードをかくとき、私はここでnext_leavesを作らずに、 while leaves:
leaf, height = leaves.pop()
...
leaves.append((neighbor, height + 1))みたいな感じのコードを一度書いて失敗しました。(0-1-2-3-4みたいな場合に、4,3,2,1,0の順に取り去ってしまってダメ)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 引っかかった点も共有いただきありがとうございます。他の方々の感覚に触れ、また自分自身で考えるきっかけになるので助かります。 |
||
| for leaf in leaves: | ||
| degrees[leaf] = 0 | ||
| remaining -= 1 | ||
|
|
||
| # Only one neighbor is active at this point since `leaf` is a leaf | ||
| for neighbor in neighbors[leaf]: | ||
| if degrees[neighbor] == 0: | ||
| continue | ||
|
|
||
| degrees[neighbor] -= 1 | ||
| if degrees[neighbor] == 1: | ||
| next_leaves.append(neighbor) | ||
|
|
||
| leaves = next_leaves | ||
|
|
||
| return leaves | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import collections | ||
|
|
||
|
|
||
| class Solution: | ||
| def findMinHeightTrees(self, n: int, edges: list[list[int]]) -> list[int]: | ||
| node_to_adjacents = collections.defaultdict(list) | ||
| for node1, node2 in edges: | ||
| node_to_adjacents[node1].append(node2) | ||
| node_to_adjacents[node2].append(node1) | ||
|
|
||
| def traverse(node: int, visited: list[bool]) -> int: | ||
| if visited[node]: | ||
| return 0 | ||
|
|
||
| visited[node] = True | ||
| child_height = 0 | ||
| for adj in node_to_adjacents[node]: | ||
| child_height = max(child_height, traverse(adj, visited)) | ||
| visited[node] = False | ||
| return 1 + child_height | ||
|
|
||
| visited = [False] * n | ||
| root_to_height = {} | ||
| for root in range(n): | ||
| root_to_height[root] = traverse(root, visited) | ||
|
|
||
| min_height = min(root_to_height.values()) | ||
| mht_roots: list[int] = [] | ||
| for root, height in root_to_height.items(): | ||
| if height == min_height: | ||
| mht_roots.append(root) | ||
| return mht_roots |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| class Solution: | ||
| def findMinHeightTrees(self, n: int, edges: list[list[int]]) -> list[int]: | ||
| if n == 1: | ||
| return [0] | ||
|
|
||
| neighbors = [set() for _ in range(n)] | ||
| for node1, node2 in edges: | ||
| neighbors[node1].add(node2) | ||
| neighbors[node2].add(node1) | ||
|
|
||
| remaining = n | ||
| leaves = [node for node in range(n) if len(neighbors[node]) == 1] | ||
| while remaining > 2: | ||
| next_leaves = [] | ||
| for leaf in leaves: | ||
| neighbor = neighbors[leaf].pop() # only one should be left | ||
| neighbors[neighbor].remove(leaf) | ||
| if len(neighbors[neighbor]) == 1: | ||
| next_leaves.append(neighbor) | ||
| remaining -= 1 | ||
|
|
||
| leaves = next_leaves | ||
|
|
||
| return leaves |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| class Solution: | ||
| def findMinHeightTrees(self, n: int, edges: list[list[int]]) -> list[int]: | ||
| if n == 1: | ||
| return [0] | ||
|
|
||
| neighbors = [set() for _ in range(n)] | ||
| for node1, node2 in edges: | ||
| neighbors[node1].add(node2) | ||
| neighbors[node2].add(node1) | ||
|
|
||
| remaining = n | ||
| leaves = [node for node in range(n) if len(neighbors[node]) == 1] | ||
| while remaining > 2: | ||
| next_leaves = [] | ||
|
|
||
| for leaf in leaves: | ||
| neighbor = neighbors[leaf].pop() | ||
| neighbors[neighbor].remove(leaf) | ||
| if len(neighbors[neighbor]) == 1: | ||
| next_leaves.append(neighbor) | ||
| remaining -= 1 | ||
|
|
||
| leaves = next_leaves | ||
|
|
||
| return leaves |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| """ | ||
| First attempt using two DFS passes to find the longest path (diameter) and return its middle node(s). | ||
| I haven't carefully reviewed or refactored yet. | ||
| TODO: | ||
| - Revisit review comments | ||
| - Improve/refactor this solution. | ||
| """ | ||
|
|
||
|
|
||
| class Solution: | ||
| def findMinHeightTrees(self, n: int, edges: list[list[int]]) -> list[int]: | ||
| neighbors = [[] for _ in range(n)] | ||
| for node1, node2 in edges: | ||
| neighbors[node1].append(node2) | ||
| neighbors[node2].append(node1) | ||
|
|
||
| def find_farthest( | ||
| node: int, visited: list[bool] | ||
| ) -> tuple[int, int]: # (farthest node, path length) | ||
| path_length = 1 | ||
| farthest = node | ||
| visited[node] = True | ||
| for neighbor in neighbors[node]: | ||
| if visited[neighbor]: | ||
| continue | ||
|
|
||
| farthest_from_neighbor, path_length_from_neighbor = find_farthest( | ||
| neighbor, visited | ||
| ) | ||
|
|
||
| if path_length_from_neighbor + 1 > path_length: | ||
| path_length = path_length_from_neighbor + 1 | ||
| farthest = farthest_from_neighbor | ||
|
|
||
| visited[node] = False | ||
| return farthest, path_length | ||
|
|
||
| longest_path_start, _ = find_farthest(0, [False] * n) | ||
| longest_path_end, longest_path_length = find_farthest( | ||
| longest_path_start, [False] * n | ||
| ) | ||
|
|
||
| longest_path = [] | ||
|
|
||
| def traverse(node: int, end: int, visited: list[bool], path: list[int]) -> None: | ||
| nonlocal longest_path | ||
|
|
||
| path.append(node) | ||
| if node == end: | ||
| longest_path = path.copy() | ||
| path.pop() | ||
| return | ||
|
|
||
| visited[node] = True | ||
|
|
||
| for neighbor in neighbors[node]: | ||
| if visited[neighbor]: | ||
| continue | ||
| traverse(neighbor, end, visited, path) | ||
|
|
||
| path.pop() | ||
| visited[node] = False | ||
|
|
||
| traverse(longest_path_start, longest_path_end, [False] * n, []) | ||
|
|
||
| if longest_path_length % 2 == 1: | ||
| return [longest_path[longest_path_length // 2]] | ||
| else: | ||
| return longest_path[ | ||
| longest_path_length // 2 - 1 : longest_path_length // 2 + 1 | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
自力で解けませんでした。
C++ で 4 * 10^8 なら TLE にならないだろうと高を括ったところ、 TLE になりました。解法が思いつかず、下記の解法を読んで解きました。難しかったです。