3607. 电网维护

题目

给你一个整数 c,表示 c 个电站,每个电站有一个唯一标识符 id,从 1 到 c 编号。

这些电站通过 n 条 双向 电缆互相连接,表示为一个二维数组 connections,其中每个元素 connections[i] = [ui, vi] 表示电站 ui 和电站 vi 之间的连接。直接或间接连接的电站组成了一个 电网 。

最初,所有 电站均处于在线(正常运行)状态。

另给你一个二维数组 queries,其中每个查询属于以下 两种类型之一 :

  • [1, x]:请求对电站 x 进行维护检查。如果电站 x 在线,则它自行解决检查。如果电站 x 已离线,则检查由与 x 同一 电网 中 编号最小 的在线电站解决。如果该电网中 不存在 任何 在线 电站,则返回 -1。
  • [2, x]:电站 x 离线(即变为非运行状态)。

返回一个整数数组,表示按照查询中出现的顺序,所有类型为 [1, x] 的查询结果。

**注意:**电网的结构是固定的;离线(非运行)的节点仍然属于其所在的电网,且离线操作不会改变电网的连接性。

示例 1:

输入: c = 5, connections = [[1,2],[2,3],[3,4],[4,5]], queries = [[1,3],[2,1],[1,1],[2,2],[1,2]]

输出: [3,2,3]

解释:

  • 最初,所有电站 {1, 2, 3, 4, 5} 都在线,并组成一个电网。
  • 查询 [1,3]:电站 3 在线,因此维护检查由电站 3 自行解决。
  • 查询 [2,1]:电站 1 离线。剩余在线电站为 {2, 3, 4, 5}
  • 查询 [1,1]:电站 1 离线,因此检查由电网中编号最小的在线电站解决,即电站 2。
  • 查询 [2,2]:电站 2 离线。剩余在线电站为 {3, 4, 5}
  • 查询 [1,2]:电站 2 离线,因此检查由电网中编号最小的在线电站解决,即电站 3。

示例 2:

输入: c = 3, connections = [], queries = [[1,1],[2,1],[1,1]]

输出: [1,-1]

解释:

  • 没有连接,因此每个电站是一个独立的电网。
  • 查询 [1,1]:电站 1 在线,且属于其独立电网,因此维护检查由电站 1 自行解决。
  • 查询 [2,1]:电站 1 离线。
  • 查询 [1,1]:电站 1 离线,且其电网中没有其他电站,因此结果为 -1。

提示:

  • 1 <= c <= 10^5
  • 0 <= n == connections.length <= min(105, c * (c - 1) / 2)
  • connections[i].length == 2
  • 1 <= ui, vi <= c
  • ui != vi
  • 1 <= queries.length <= 2 * 10%5
  • queries[i].length == 2
  • queries[i][0] 为 1 或 2。
  • 1 <= queries[i][1] <= c

解题思路

最朴素的想法是:对每个查询 [1, x],在整张图里做一次 BFS/DFS 找到和 x 处于同一电网(连通分量)的所有节点,然后在这些节点里找 最小编号的在线电站;对 [2, x] 就把 x 标为离线即可。

  • 性能分析: 设电站数为 (C)、电缆数为 (N)、查询数为 (Q)。一次 BFS/DFS 的时间是 (O(C+N))。如果很多查询都是 [1, x],最坏会到 (O(Q\cdot (C+N)))。当三者都可达 (10^5) 量级时,这是不可接受的(必超时)。
  • 关键观察: 题目已经强调 “离线不会改变连接性”,也就是说图的连通分量是静态的。 👉 因此,只需要在开头 一次性 把所有连通分量(电网)分出来,之后就绝不再做整图搜索。
  • 对策:空间换时间(预处理 + 快速查询)
    1. 连通分量预处理:用 并查集(Union-Find/DSU) 或一次性 BFS/DFS,把每个电站映射到一个电网 ID(根)。这步只做一遍,复杂度 (O(C+N))。
    2. 电网内“最小在线编号”的快速获取:为每个电网维护一个 最小堆(min-heap),把该电网内的所有节点编号都放进去。堆顶就是“最小编号”。
    3. 在线状态:用一个布尔数组 online[1..C] 标记在线/离线。下线时只改布尔值,不立刻改堆。
    4. 查询 [1, x]
      • x 在线,直接返回 x
      • x 离线,则到 x 的电网堆中“懒惰删除”离线堆顶,直到堆顶为在线节点或堆空:
        • 堆空 → 返回 -1
        • 否则 → 返回堆顶。
    5. 查询 [2, x]:把 online[x] = False从堆里删除 x,等下次需要取堆顶再顺便清理——懒删除)。
  • 为什么用堆? 我们需要“电网内最小在线编号”的即时查询。维护有序集合的方式很多(如 TreeSet/set),但用最小堆配合懒删除足够简单高效:
    • 取最小:摊还 (O(\log C));
    • 离线后不做删除:把成本推迟到真正需要时;每个节点最多被弹出一次,全程总开销 (O(C \log C))

总时间复杂度

  • 预处理连通分量:(O(C+N));
  • 建堆:把每个电网的节点各入堆一次,总计 (O(C)) 建堆(摊还)或 (O(C \log C))(逐个 push);
  • 处理 (Q) 次查询:
    • [2, x]:(O(1));
    • [1, x]:均摊至 (O(\log C))(堆顶清理 + 取顶);
  • 综合:(O(C+N) + O(C) + O(Q\log C))(或写成 (O((C+N+Q)\log C)) 也常见)。对 (10^5) 量级可轻松通过。

懒删除的堆技巧

链表题里删除需要“前驱指针”与“哨兵节点”; 在本题里,删除(下线)对应的是“把节点从候选集合里移除”。如果我们真的在堆里做“任意位置删除”,实现会复杂、常数也大。更优雅的办法**是:

  • 懒惰删除(Lazy Removal)
    • 下线时:只做 online[x] = False动堆。
    • 需要取“最小在线编号”时:
      • 看看堆顶 (h[0]) 是否在线;
      • 若离线,就 heappop(h) 弹掉,继续看新的堆顶;
      • 重复直到堆顶在线或堆空。
    • 这样每个节点最多被弹出一次,总弹出次数 ≤ C,均摊下来非常高效。
  • 并查集的作用(对标链表里“指针指向关系”): 链表里“是否相连”由 next 指针决定;本题里“是否在同一电网”由连通性决定。 我们用 DSU 一次性 固定每个节点的“代表根”,就像给每个节点打上了“电网标签”。 后续任何查询 [1, x][2, x] 都可以 O(1) 地定位到“电网 ID = find(x)”,从而直达对应的最小堆做操作。

实现要点小结

  1. DSU 预处理find(x) 返回电网 ID(根)。
  2. 为每个电网建一个最小堆:初始把该电网的所有节点都放入;
  3. 在线数组online[x] = True/False
  4. 查询
    • [1, x]
      • online[x]:返回 x
      • 否则:到 comp_heap[find(x)] 里懒删离线堆顶,取当前堆顶或 -1
    • [2, x]online[x] = False(不操作堆)。

具体代码

CPP

class Solution {
public:
    vector<int> processQueries(int c, vector<vector<int>>& connections, vector<vector<int>>& queries) {
        // --- 并查集(Union-Find) ---
        vector<int> parent(c + 1), rnk(c + 1, 0);
        iota(parent.begin(), parent.end(), 0);

        auto find = [&](int x) {
            // 路径压缩
            int y = x;
            while (parent[y] != y) y = parent[y];
            while (parent[x] != x) {
                int p = parent[x];
                parent[x] = y;
                x = p;
            }
            return y;
        };

        auto uni = [&](int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) return;
            // 按秩合并
            if (rnk[ra] < rnk[rb]) {
                parent[ra] = rb;
            } else if (rnk[ra] > rnk[rb]) {
                parent[rb] = ra;
            } else {
                parent[rb] = ra;
                rnk[ra]++;
            }
        };

        // 1) 用并查集合并所有连接(电网结构固定)
        for (auto &e : connections) uni(e[0], e[1]);

        // 2) 为每个电网维护一个最小堆(节点编号最小优先)
        using MinHeap = priority_queue<int, vector<int>, greater<int>>;
        vector<MinHeap> compHeap(c + 1);
        for (int i = 1; i <= c; ++i) {
            int r = find(i);
            compHeap[r].push(i);
        }

        // 3) 记录在线状态,初始全部在线
        vector<char> online(c + 1, 1);

        // 4) 处理查询;对类型为 [1, x] 的查询输出结果
        vector<int> ans;
        ans.reserve(queries.size());
        for (auto &q : queries) {
            int t = q[0], x = q[1];
            int r = find(x);
            if (t == 1) {
                // 维护检查:
                if (online[x]) {
                    // 如果 x 在线,由自己处理
                    ans.push_back(x);
                } else {
                    // 否则找该电网中编号最小的在线节点(懒惰删除离线节点)
                    auto &h = compHeap[r];
                    while (!h.empty() && !online[h.top()]) h.pop();
                    if (h.empty()) ans.push_back(-1);
                    else ans.push_back(h.top());
                }
            } else {
                // 下线操作:
                if (online[x]) {
                    online[x] = 0;
                    // 不必从堆中立即移除,查询时懒惰删除
                }
            }
        }
        return ans;
    }
};

Python

from typing import List
import heapq

class Solution:
    def processQueries(self, c: int, connections: List[List[int]], queries: List[List[int]]) -> List[int]:
        # --- 并查集(Union-Find) ---
        parent = list(range(c + 1))
        rank = [0] * (c + 1)

        def find(x: int) -> int:
            # 路径压缩(加速查找根节点)
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        def union(a: int, b: int) -> None:
            # 按秩合并(rank小的挂到rank大的下面)
            ra, rb = find(a), find(b)
            if ra == rb:
                return
            if rank[ra] < rank[rb]:
                parent[ra] = rb
            elif rank[ra] > rank[rb]:
                parent[rb] = ra
            else:
                parent[rb] = ra
                rank[ra] += 1

        # 第一步:通过并查集找到每个电站所在的连通分量(电网)
        for u, v in connections:
            union(u, v)

        # 第二步:为每个连通分量维护一个最小堆(存储该电网中的所有节点)
        comp_heap = {}  # root -> 节点最小堆
        for node in range(1, c + 1):
            r = find(node)
            if r not in comp_heap:
                comp_heap[r] = []
            comp_heap[r].append(node)

        # 将每个电网内的节点建堆
        for r in comp_heap:
            heapq.heapify(comp_heap[r])

        # 第三步:记录每个电站是否在线
        online = [True] * (c + 1)  # 下标从1开始

        # 第四步:处理查询
        res = []
        for t, x in queries:
            r = find(x)
            if t == 1:
                # 查询类型1:维护检查
                if online[x]:
                    # 如果电站x在线,由自己处理
                    res.append(x)
                else:
                    # 如果离线,查找同电网中编号最小的在线电站
                    h = comp_heap.get(r, [])
                    # 懒惰删除(lazy removal):移除堆顶离线节点
                    while h and not online[h[0]]:
                        heapq.heappop(h)
                    res.append(h[0] if h else -1)
            else:
                # 查询类型2:将电站x设为离线
                if online[x]:
                    online[x] = False
                # 不需要立即从堆中删除,下一次查询时懒惰删除即可

        return res