diff --git a/leetcode/310/memo.md b/leetcode/310/memo.md new file mode 100644 index 0000000..b873b20 --- /dev/null +++ b/leetcode/310/memo.md @@ -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? + +幾つか例を考えてみると、雑に考えてグラフの端から端までいく最長のパスかあって、それの真ん中のノードが 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) になる。 + +やり方だけ頭において、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. diff --git a/leetcode/310/step1.py b/leetcode/310/step1.py new file mode 100644 index 0000000..e1e8d8f --- /dev/null +++ b/leetcode/310/step1.py @@ -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 = [] + 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 diff --git a/leetcode/310/step1_tle.py b/leetcode/310/step1_tle.py new file mode 100644 index 0000000..3154464 --- /dev/null +++ b/leetcode/310/step1_tle.py @@ -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 diff --git a/leetcode/310/step2.py b/leetcode/310/step2.py new file mode 100644 index 0000000..c377f5d --- /dev/null +++ b/leetcode/310/step2.py @@ -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 diff --git a/leetcode/310/step3.py b/leetcode/310/step3.py new file mode 100644 index 0000000..5adc97d --- /dev/null +++ b/leetcode/310/step3.py @@ -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 diff --git a/leetcode/310/step4_dfs_twice_and_get_middle.py b/leetcode/310/step4_dfs_twice_and_get_middle.py new file mode 100644 index 0000000..ac2e5fa --- /dev/null +++ b/leetcode/310/step4_dfs_twice_and_get_middle.py @@ -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 + ]