|
| 1 | +# 3607. Power Grid Maintenance |
| 2 | + |
| 3 | +You are given an integer `c` representing `c` power stations, each with a unique identifier `id` from 1 to `c` (1‑based indexing). |
| 4 | + |
| 5 | +These stations are interconnected via n bidirectional cables, represented by a 2D array connections, |
| 6 | +where each element `connections[i] = [u_i, v_i]` indicates a connection between station `u_i` and station `v_i`. |
| 7 | +Stations that are directly or indirectly connected form a power grid. |
| 8 | + |
| 9 | +Initially, all stations are online (operational). |
| 10 | + |
| 11 | +You are also given a 2D array `queries`, where each query is one of the following two types: |
| 12 | + |
| 13 | +- `[1, x]`: A maintenance check is requested for station `x`. |
| 14 | + If station `x` is online, it resolves the check by itself. |
| 15 | + If station `x` is offline, the check is resolved by the operational station with the smallest `id` in the same power grid as `x`. |
| 16 | + If no operational station exists in that grid, return -1. |
| 17 | + |
| 18 | +- `[2, x]`: Station `x` goes offline (i.e., it becomes non-operational). |
| 19 | + |
| 20 | +Return an array of integers representing the results of each query of type `[1, x]` in the order they appear. |
| 21 | + |
| 22 | +Note: The power grid preserves its structure; an offline (non‑operational) node remains part of its grid and taking it offline does not alter connectivity. |
| 23 | + |
| 24 | +**Constraints:** |
| 25 | + |
| 26 | +- `1 <= c <= 10^5` |
| 27 | +- `0 <= n == connections.length <= min(10^5, c * (c - 1) / 2)` |
| 28 | +- `connections[i].length == 2` |
| 29 | +- `1 <= u_i, v_i <= c` |
| 30 | +- `u_i != v_i` |
| 31 | +- `1 <= queries.length <= 2 * 10^5` |
| 32 | +- `queries[i].length == 2` |
| 33 | +- `queries[i][0]` is either `1` or `2`. |
| 34 | +- `1 <= queries[i][1] <= c` |
| 35 | + |
| 36 | +## 基礎思路 |
| 37 | + |
| 38 | +本題要我們模擬一個電網系統,支援「查詢維修」與「停機」操作,並且在大量節點與查詢下維持高效執行。 |
| 39 | + |
| 40 | +題意可歸納為: |
| 41 | + |
| 42 | +1. 每個電站具有唯一 ID(1-based)。 |
| 43 | +2. 電站之間以雙向電纜連線,形成若干「電網(連通分量)」。 |
| 44 | +3. 每次查詢可能是: |
| 45 | + - **`[1, x]` 維修請求**:若 `x` 在線上,輸出 `x`;若 `x` 離線,輸出同一電網中最小編號的線上電站;若整網皆離線,輸出 `-1`。 |
| 46 | + - **`[2, x]` 停機請求**:將電站 `x` 標記為離線。 |
| 47 | +4. 電網結構不會因停機而改變(停機不會斷線)。 |
| 48 | + |
| 49 | +在思考解法時,我們需關注幾個重點: |
| 50 | + |
| 51 | +- **電網識別**:要能快速判斷兩電站是否屬於同一電網。 |
| 52 | +- **最小線上節點查找**:需能在某個電網內找到最小線上電站,且當節點停機後仍能有效更新。 |
| 53 | +- **高效查詢**:題目上限達 $2 \times 10^5$ 次查詢,必須避免逐次遍歷整個電網。 |
| 54 | + |
| 55 | +為達成以上要求,我們採取以下策略: |
| 56 | + |
| 57 | +- **使用並查集(Union-Find)**:先將所有電站依照電纜連線合併成連通分量,確定每個電站所屬電網。 |
| 58 | +- **分量壓縮與排序**:將每個電網的電站以 ID 升序排列,並記錄每個電網的起訖區間位置。 |
| 59 | +- **指標維護**:對每個電網維護一個指標,指向目前最小的線上節點;若該節點停機,指標向後推進。 |
| 60 | +- **懶惰推進策略(Lazy advancement)**:只有當某電網被查詢或停機時才更新指標,避免重複掃描。 |
| 61 | + |
| 62 | +透過此設計,查詢與停機皆可在近似常數時間內完成,滿足題目的效能需求。 |
| 63 | + |
| 64 | +## 解題步驟 |
| 65 | + |
| 66 | +### Step 1:處理無電纜情況 |
| 67 | + |
| 68 | +若沒有任何電纜,所有電站皆獨立成網,查詢僅需判斷該站是否離線。 |
| 69 | + |
| 70 | +```typescript |
| 71 | +// 若無任何連線,每個電站皆獨立,直接以單節點方式處理 |
| 72 | +if (edgeCount === 0) { |
| 73 | + const isOffline = new Uint8Array(stationCount + 1); // 標記每個電站是否離線 |
| 74 | + const results: number[] = []; |
| 75 | + |
| 76 | + for (let i = 0; i < queries.length; i += 1) { |
| 77 | + const queryType = queries[i][0] | 0; |
| 78 | + const stationId = queries[i][1] | 0; |
| 79 | + |
| 80 | + if (queryType === 2) { |
| 81 | + // 將指定電站標記為離線 |
| 82 | + isOffline[stationId] = 1; |
| 83 | + continue; |
| 84 | + } |
| 85 | + |
| 86 | + // 維修查詢:若在線上輸出自身,否則輸出 -1 |
| 87 | + if (isOffline[stationId] === 0) { |
| 88 | + results.push(stationId); |
| 89 | + } else { |
| 90 | + results.push(-1); |
| 91 | + } |
| 92 | + } |
| 93 | + return results; |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +### Step 2:建立並查集 |
| 98 | + |
| 99 | +並查集(Union-Find)能快速判定兩節點是否屬於同一電網。 |
| 100 | + |
| 101 | +```typescript |
| 102 | +// 建立並查集結構 |
| 103 | +const parent = new Int32Array(stationCount + 1); |
| 104 | +const setSize = new Int32Array(stationCount + 1); |
| 105 | + |
| 106 | +// 初始化:每個節點自成一組 |
| 107 | +for (let stationId = 1; stationId <= stationCount; stationId += 1) { |
| 108 | + parent[stationId] = stationId; |
| 109 | + setSize[stationId] = 1; |
| 110 | +} |
| 111 | + |
| 112 | +/** |
| 113 | + * 尋找節點的根節點(帶路徑壓縮) |
| 114 | + */ |
| 115 | +function findRoot(stationId: number): number { |
| 116 | + let current = stationId | 0; |
| 117 | + while (parent[current] !== current) { |
| 118 | + parent[current] = parent[parent[current]]; // 路徑壓縮 |
| 119 | + current = parent[current]; |
| 120 | + } |
| 121 | + return current; |
| 122 | +} |
| 123 | + |
| 124 | +/** |
| 125 | + * 合併兩個集合(按大小合併以保持樹淺) |
| 126 | + */ |
| 127 | +function mergeSets(firstStation: number, secondStation: number): void { |
| 128 | + let rootA = findRoot(firstStation); |
| 129 | + let rootB = findRoot(secondStation); |
| 130 | + |
| 131 | + if (rootA === rootB) return; // 已屬同一電網 |
| 132 | + |
| 133 | + if (setSize[rootA] < setSize[rootB]) { |
| 134 | + const temp = rootA; |
| 135 | + rootA = rootB; |
| 136 | + rootB = temp; |
| 137 | + } |
| 138 | + |
| 139 | + parent[rootB] = rootA; |
| 140 | + setSize[rootA] += setSize[rootB]; |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +### Step 3:合併所有連線 |
| 145 | + |
| 146 | +依序合併每條連線,將相連的電站合併為同一電網。 |
| 147 | + |
| 148 | +```typescript |
| 149 | +// 根據連線建立電網結構 |
| 150 | +for (let i = 0; i < edgeCount; i += 1) { |
| 151 | + const u = connections[i][0] | 0; |
| 152 | + const v = connections[i][1] | 0; |
| 153 | + mergeSets(u, v); |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +### Step 4:壓縮電網索引 |
| 158 | + |
| 159 | +將每個根節點分配獨立的電網編號,建立「節點 → 電網」的對應關係。 |
| 160 | + |
| 161 | +```typescript |
| 162 | +// 將每個電網根節點映射成獨立的電網編號 |
| 163 | +const rootToComponent = new Int32Array(stationCount + 1); |
| 164 | +let componentCount = 0; |
| 165 | + |
| 166 | +for (let stationId = 1; stationId <= stationCount; stationId += 1) { |
| 167 | + const root = findRoot(stationId); |
| 168 | + if (rootToComponent[root] === 0) { |
| 169 | + componentCount += 1; |
| 170 | + rootToComponent[root] = componentCount; |
| 171 | + } |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +### Step 5:建立每個電網的節點清單 |
| 176 | + |
| 177 | +依電網索引將電站排序並集中儲存,方便後續快速查詢最小線上節點。 |
| 178 | + |
| 179 | +```typescript |
| 180 | +// 建立電網中電站的排序資訊 |
| 181 | +const stationToComponent = new Int32Array(stationCount + 1); |
| 182 | +const orderedStations = new Int32Array(stationCount); |
| 183 | +const componentSize = new Int32Array(componentCount + 1); |
| 184 | + |
| 185 | +// 統計每個電網的節點數 |
| 186 | +for (let id = 1; id <= stationCount; id += 1) { |
| 187 | + const root = findRoot(id); |
| 188 | + const comp = rootToComponent[root]; |
| 189 | + stationToComponent[id] = comp; |
| 190 | + componentSize[comp] += 1; |
| 191 | +} |
| 192 | + |
| 193 | +// 計算每個電網在 orderedStations 中的起訖位置(prefix sum) |
| 194 | +const start = new Int32Array(componentCount + 1); |
| 195 | +const end = new Int32Array(componentCount + 1); |
| 196 | +let offset = 0; |
| 197 | +for (let comp = 1; comp <= componentCount; comp += 1) { |
| 198 | + start[comp] = offset; |
| 199 | + offset += componentSize[comp]; |
| 200 | + end[comp] = offset; |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +### Step 6:填入每個電網的節點並初始化指標 |
| 205 | + |
| 206 | +每個電網的節點在 `orderedStations` 內依 ID 升序排列,並為每個電網設置指標指向最小線上節點。 |
| 207 | + |
| 208 | +```typescript |
| 209 | +// 每個電網的填寫游標 |
| 210 | +const writeCursor = new Int32Array(componentCount + 1); |
| 211 | +for (let comp = 1; comp <= componentCount; comp += 1) { |
| 212 | + writeCursor[comp] = start[comp]; |
| 213 | +} |
| 214 | + |
| 215 | +// 將節點依序填入 orderedStations |
| 216 | +for (let id = 1; id <= stationCount; id += 1) { |
| 217 | + const comp = stationToComponent[id]; |
| 218 | + const pos = writeCursor[comp]; |
| 219 | + orderedStations[pos] = id; |
| 220 | + writeCursor[comp] = pos + 1; |
| 221 | +} |
| 222 | + |
| 223 | +// 初始化每個電網的指標 |
| 224 | +const pointer = new Int32Array(componentCount + 1); |
| 225 | +for (let comp = 1; comp <= componentCount; comp += 1) { |
| 226 | + pointer[comp] = start[comp]; |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +### Step 7:定義輔助函數 |
| 231 | + |
| 232 | +該函數用來在某電網內前進指標,跳過所有已離線節點。 |
| 233 | + |
| 234 | +```typescript |
| 235 | +/** |
| 236 | + * 移動電網指標以跳過離線節點 |
| 237 | + */ |
| 238 | +function movePointerForward(componentIndex: number): void { |
| 239 | + let p = pointer[componentIndex]; |
| 240 | + const endPos = end[componentIndex]; |
| 241 | + |
| 242 | + while (p < endPos) { |
| 243 | + const id = orderedStations[p]; |
| 244 | + if (isOffline[id] === 0) break; // 找到線上節點 |
| 245 | + p += 1; |
| 246 | + } |
| 247 | + pointer[componentIndex] = p; |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +### Step 8:處理所有查詢 |
| 252 | + |
| 253 | +根據查詢類型更新離線狀態或回傳維修結果。 |
| 254 | + |
| 255 | +```typescript |
| 256 | +// 處理查詢 |
| 257 | +const isOffline = new Uint8Array(stationCount + 1); |
| 258 | +const results: number[] = []; |
| 259 | + |
| 260 | +for (let i = 0; i < queries.length; i += 1) { |
| 261 | + const type = queries[i][0] | 0; |
| 262 | + const id = queries[i][1] | 0; |
| 263 | + |
| 264 | + if (type === 2) { |
| 265 | + // 停機事件:標記離線並更新指標 |
| 266 | + isOffline[id] = 1; |
| 267 | + const comp = stationToComponent[id]; |
| 268 | + const p = pointer[comp]; |
| 269 | + if (p < end[comp] && orderedStations[p] === id) { |
| 270 | + movePointerForward(comp); |
| 271 | + } |
| 272 | + continue; |
| 273 | + } |
| 274 | + |
| 275 | + // 維修查詢 |
| 276 | + if (isOffline[id] === 0) { |
| 277 | + results.push(id); // 在線上,輸出自身 |
| 278 | + continue; |
| 279 | + } |
| 280 | + |
| 281 | + // 離線:尋找同電網中最小的線上電站 |
| 282 | + const comp = stationToComponent[id]; |
| 283 | + movePointerForward(comp); |
| 284 | + const pNow = pointer[comp]; |
| 285 | + if (pNow >= end[comp]) { |
| 286 | + results.push(-1); // 全電網離線 |
| 287 | + } else { |
| 288 | + results.push(orderedStations[pNow]); // 回傳最小線上節點 |
| 289 | + } |
| 290 | +} |
| 291 | +``` |
| 292 | + |
| 293 | +## 時間複雜度 |
| 294 | + |
| 295 | +- **預處理(並查集 + 佈局)**: |
| 296 | + 建立 DSU 並合併所有連邊為 $O(m,\alpha(n))$,壓縮與分塊佈局為 $O(n)$。 |
| 297 | + 其中 $m$ 為電纜數,$n$ 為電站數,$\alpha$ 為阿克曼反函數。 |
| 298 | +- **線上查詢**: |
| 299 | + 每個連通塊的游標總前移次數至多等於該塊站數,因此所有 `[1]` 與 `[2]` 操作的總攤銷時間為 $O(q + n)$。 |
| 300 | + 其中 $q$ 為查詢數。 |
| 301 | +- 總時間複雜度為 $O(n + m,\alpha(n) + q)$。 |
| 302 | + |
| 303 | +> $O(n + m,\alpha(n) + q)$ |
| 304 | +
|
| 305 | +## 空間複雜度 |
| 306 | + |
| 307 | +- DSU 陣列、電網分塊、排序後站台、游標與離線標記陣列皆為線性規模。 |
| 308 | +- 需額外 $O(n)$ 儲存索引與輔助結構。 |
| 309 | +- 總空間複雜度為 $O(n)$。 |
| 310 | + |
| 311 | +> $O(n)$ |
0 commit comments