|
| 1 | +# 684. Redundant Connection |
| 2 | + |
| 3 | +In this problem, a tree is an undirected graph that is connected and has no cycles. |
| 4 | + |
| 5 | +You are given a graph that started as a tree with `n` nodes labeled from `1` to `n`, with one additional edge added. |
| 6 | +The added edge has two different vertices chosen from `1` to `n`, and was not an edge that already existed. |
| 7 | +The graph is represented as an array edges of length `n` where `edges[i] = [a_i, b_i]` indicates that |
| 8 | +there is an edge between nodes `a_i` and `b_i` in the graph. |
| 9 | + |
| 10 | +Return an edge that can be removed so that the resulting graph is a tree of `n` nodes. |
| 11 | +If there are multiple answers, return the answer that occurs last in the input. |
| 12 | + |
| 13 | +## 基礎思路 |
| 14 | +我們可以換個角度來看這個問題,我們可以將這個問題轉換成尋找一個圖中的cycle,並且返回這個cycle中的最後一個邊。 |
| 15 | + |
| 16 | +> Tips: |
| 17 | +> - 為了節省時間,我們可以使用 DSU (Disjoint Set Union) 來實現這個問題。 |
| 18 | +> - 在圖中,如果兩個節點之間已經通過其他邊相連,它們會屬於同一個集合。 |
| 19 | +> - 如果我們再加入一個邊,且這個邊的兩個節點屬於同一個集合,那麼這個邊就是多餘的。 |
| 20 | +> - 為了節省空間,我們能用單個陣列追蹤每個節點的父節點,來代表集合。 |
| 21 | +
|
| 22 | +## 解題步驟 |
| 23 | + |
| 24 | +### Step 1: 定義查找 root 的函數 |
| 25 | + |
| 26 | +```typescript |
| 27 | +const findRoot = (parent: number[], node: number): number => { |
| 28 | + if (parent[node] !== node) { |
| 29 | + // 路徑壓縮:將當前節點的父節點指向集合的根節點 |
| 30 | + parent[node] = findRoot(parent, parent[node]); |
| 31 | + } |
| 32 | + return parent[node]; |
| 33 | +}; |
| 34 | +``` |
| 35 | + |
| 36 | +### Step 2: 定義合併集合的函數 |
| 37 | + |
| 38 | +```typescript |
| 39 | +const unionSets = (parent: number[], node1: number, node2: number): void => { |
| 40 | + // 將 node1 的根節點指向 node2 的根節點 |
| 41 | + parent[findRoot(parent, node1)] = findRoot(parent, node2); |
| 42 | +}; |
| 43 | +``` |
| 44 | + |
| 45 | +### Step 3: 初始化 parent 陣列 |
| 46 | + |
| 47 | +```typescript |
| 48 | +// 我們初始化一個長度為 edges.length + 1 的陣列,並且將每個節點的父節點設置為自己 |
| 49 | +const parent = new Array(edges.length + 1).fill(0).map((_, index) => index); |
| 50 | +``` |
| 51 | + |
| 52 | +### Step 4: 遍歷邊,並且判斷是否有 cycle |
| 53 | + |
| 54 | +```typescript |
| 55 | +for (const [node1, node2] of edges) { |
| 56 | + // 如果兩個節點有相同的根節點,則這個邊是多餘的 |
| 57 | + if (findRoot(parent, node1) === findRoot(parent, node2)) { |
| 58 | + return [node1, node2]; |
| 59 | + } |
| 60 | + // 否則,我們將這兩個節點合併到同一個集合中 |
| 61 | + unionSets(parent, node1, node2); |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +## Step 5: 當沒有 cycle 時,返回空陣列 |
| 66 | + |
| 67 | +這是一個好習慣,確保我們的程式碼能夠處理所有的情況。即便題目中不會出現這種情況,但是我們還是應該要考慮到這種情況。 |
| 68 | + |
| 69 | +```typescript |
| 70 | +return []; |
| 71 | +``` |
| 72 | + |
| 73 | +## 時間複雜度 |
| 74 | + |
| 75 | +- 每次 findRoot 和 unionSets 操作的時間複雜度為 $O(\alpha(n))$,其中 $\alpha(n)$ 是[阿克曼函數](https://en.wikipedia.org/wiki/Ackermann_function)的反函數。由於 $\alpha(n)$ 的增長速度極慢,在實際應用中可視為常數時間。 |
| 76 | +- 總體遍歷所有邊的操作 需要進行 $n$ 次 findRoot 和 unionSets 操作。時間複雜度為 $O(n \cdot \alpha(n))$。 |
| 77 | +- 總體時間複雜度為 $O(n \cdot \alpha(n))$。 |
| 78 | + |
| 79 | +> $O(n \cdot \alpha(n))$ |
| 80 | +
|
| 81 | +## 空間複雜度 |
| 82 | + |
| 83 | +- Parent 陣列的空間複雜度為 $O(n)$ |
| 84 | +- 其他變數的空間複雜度為 $O(1)$ |
| 85 | +- 總體空間複雜度為 $O(n)$ |
| 86 | + |
| 87 | +> $O(n)$ |
0 commit comments