|
| 1 | +# 2127. Maximum Employees to Be Invited to a Meeting |
| 2 | + |
| 3 | +A company is organizing a meeting and has a list of `n` employees, waiting to be invited. |
| 4 | +They have arranged for a large circular table, capable of seating any number of employees. |
| 5 | + |
| 6 | +The employees are numbered from `0` to `n - 1`. |
| 7 | +Each employee has a favorite person, and they will attend the meeting only if they can sit next to their favorite person at the table. |
| 8 | +The favorite person of an employee is not themselves. |
| 9 | + |
| 10 | +Given a 0-indexed integer array `favorite`, where `favorite[i]` denotes the favorite person of the $i_{th}$ employee, |
| 11 | +return the maximum number of employees that can be invited to the meeting. |
| 12 | + |
| 13 | +## 基礎思路 |
| 14 | + |
| 15 | +我們可以將問題轉化為一個「有向圖」(directed graph) 的問題: |
| 16 | +- 節點 (node) 代表員工。 |
| 17 | +- 每個員工指向自己最喜歡的人 (favorite[i]),形成有向邊。 |
| 18 | + |
| 19 | +在一張圓桌上,如果一組員工能圍成一個「迴圈」(cycle),那麼就能彼此相鄰。 |
| 20 | +所以有兩種情形可以幫我們找最大可邀請人數: |
| 21 | +1. **尋找圖中最長的迴圈 (cycle)**。 |
| 22 | +2. **尋找所有「互相喜歡」的 2-cycle (雙人互相指向) 加上各自延伸進來的「鏈」(chain) 長度後,再把它們合計起來。** |
| 23 | + |
| 24 | +> Tips |
| 25 | +> - 若整個圖中最大的迴圈長度大於所有 2-cycle 加它們外圍鏈的總合,答案就是最大迴圈。 |
| 26 | +> - 否則,我們就選擇所有 2-cycle 配上它們的鏈,並把總長加起來。 |
| 27 | +> - 兩者取其最大值即為答案。 |
| 28 | +
|
| 29 | +## 解題步驟 |
| 30 | + |
| 31 | +以下步驟對應到主要程式邏輯,並附上重點程式片段說明。 |
| 32 | + |
| 33 | +### Step 1: 預處理入度與初始化鏈長度 |
| 34 | + |
| 35 | +1. 計算每個節點的入度,記錄有多少人最喜歡該節點。 |
| 36 | +2. 初始化所有節點的鏈長度為 `1`(因為自身就構成最短的鏈)。 |
| 37 | + |
| 38 | +```typescript |
| 39 | +// 紀錄員工人數 |
| 40 | +const n = favorite.length; |
| 41 | + |
| 42 | +// 紀錄每個人的入度 (被多少人喜歡) |
| 43 | +const inDegree = Array(n).fill(0); |
| 44 | + |
| 45 | +// 計算每個節點的入度 (被多少人喜歡) |
| 46 | +for (const person of favorite) { |
| 47 | + inDegree[person] += 1; |
| 48 | +} |
| 49 | + |
| 50 | +// 陣列紀錄節點是否被拜訪過,以及每個節點的最長鏈長度 |
| 51 | +const visited = Array(n).fill(false); |
| 52 | +const longestChain = Array(n).fill(1); |
| 53 | +``` |
| 54 | + |
| 55 | +### Step 2: 拓撲排序處理鏈長度 |
| 56 | + |
| 57 | +1. 將入度為 `0` 的節點加入隊列,並標記為已拜訪。 |
| 58 | +2. 不斷處理隊列中的節點,更新其「鏈長度」到下一個節點,並減少下一個節點的入度。 |
| 59 | +3. 如果下一個節點的入度變為 `0`,加入隊列。 |
| 60 | + |
| 61 | +```typescript |
| 62 | +// 初始化隊列 |
| 63 | +const queue: number[] = []; |
| 64 | + |
| 65 | +// 將所有入度為 0 (沒有人喜歡) 的節點加入隊列,並標記為已拜訪 |
| 66 | +for (let i = 0; i < n; i++) { |
| 67 | + if (inDegree[i] === 0) { |
| 68 | + queue.push(i); |
| 69 | + visited[i] = true; |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +// 進行拓撲排序,計算鏈長度 |
| 74 | +while (queue.length > 0) { |
| 75 | + const currentNode = queue.pop()!; |
| 76 | + const nextNode = favorite[currentNode]; |
| 77 | + |
| 78 | + // 若把 currentNode 接在 nextNode 前面,可以使 longestChain[nextNode] 更長 |
| 79 | + longestChain[nextNode] = Math.max(longestChain[nextNode], longestChain[currentNode] + 1); |
| 80 | + |
| 81 | + // 因為使用了 currentNode 這條邊,所以 nextNode 的入度要減 1 |
| 82 | + inDegree[nextNode]--; |
| 83 | + |
| 84 | + // 如果 nextNode 的入度歸 0,則放入 queue,並標記為已拜訪 |
| 85 | + if (inDegree[nextNode] === 0) { |
| 86 | + queue.push(nextNode); |
| 87 | + visited[nextNode] = true; |
| 88 | + } |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +### Step 3: 計算圖中的迴圈與 2-cycle |
| 93 | + |
| 94 | +1. 遍歷所有節點,找出尚未處理過的迴圈。 |
| 95 | +2. 對於長度大於 `2` 的迴圈,記錄最大迴圈長度。 |
| 96 | +3. 對於 2-cycle,計算兩邊鏈的總長度並累加。 |
| 97 | + |
| 98 | +> Tips |
| 99 | +> 我們在該步驟中同時紀錄最大迴圈長度與所有 2-cycle 的鏈長度總和。 |
| 100 | +> 這能提升效率。 |
| 101 | +
|
| 102 | +```typescript |
| 103 | +// 最大迴圈長度 |
| 104 | +let maxCycleLength = 0; |
| 105 | + |
| 106 | +// 所有 2-cycle 的鏈長度總和 |
| 107 | +let twoCycleChainLength = 0; |
| 108 | + |
| 109 | +// 遍歷所有節點,找出迴圈與 2-cycle 的鏈長度 |
| 110 | +for (let i = 0; i < n; i++) { |
| 111 | + // 若節點已經被拜訪過,則跳過 |
| 112 | + if (visited[i]) continue; |
| 113 | + |
| 114 | + let currentNode = i; |
| 115 | + let cycleLength = 0; |
| 116 | + |
| 117 | + // 遍歷迴圈,並標記節點為已拜訪 |
| 118 | + while (!visited[currentNode]) { |
| 119 | + visited[currentNode] = true; |
| 120 | + currentNode = favorite[currentNode]; |
| 121 | + cycleLength++; |
| 122 | + } |
| 123 | + |
| 124 | + // 若迴圈長度大於 2,則更新最大迴圈長度 |
| 125 | + if (cycleLength > 2) { |
| 126 | + maxCycleLength = Math.max(maxCycleLength, cycleLength); |
| 127 | + } |
| 128 | + // 若剛好是 2-cycle,則計算兩邊鏈的總長度 |
| 129 | + else if (cycleLength === 2) { |
| 130 | + const node1 = i; |
| 131 | + const node2 = favorite[i]; |
| 132 | + twoCycleChainLength += longestChain[node1] + longestChain[node2]; |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### Step 4: 返回最大值 |
| 138 | + |
| 139 | +將最大迴圈長度與所有 2-cycle 加上其鏈的總長度比較,取最大值作為答案。 |
| 140 | + |
| 141 | +```typescript |
| 142 | +return Math.max(maxCycleLength, twoCycleChainLength); |
| 143 | +``` |
| 144 | + |
| 145 | +## 時間複雜度 |
| 146 | +- 計算入度與初始化鏈長度需要 $O(n)$。 |
| 147 | +- 拓撲排序處理鏈長度需要 $O(n)$,因為每條邊只處理一次。 |
| 148 | +- 找出所有迴圈與 2-cycle 需要 $O(n)$。 |
| 149 | + |
| 150 | +> $O(n)$ |
| 151 | +
|
| 152 | +## 空間複雜度 |
| 153 | +- `inDegree`、`visited`、`longestChain` 等陣列需要 $O(n)$。 |
| 154 | +- 拓撲排序隊列的空間最多為 $O(n)$。 |
| 155 | + |
| 156 | +> $O(n)$ |
0 commit comments