图论专题(二十一):并查集的“工程应用”——拔线重连,修复「连通网络」

哈喽各位,我是前端小L。

欢迎来到我们的图论专题第二十一篇!在现实世界的网络工程中,资源往往是有限的。如果我们发现公司里的电脑网络分成了好几个互不相通的局域网,而手头的网线又不够用,该怎么办?

我们必须通过"拆东墙补西墙"的策略:找到那些"多余"的网线(在一个连通分量内部形成环的线),把它们拔下来,用来连接那些断开的局域网。

这道题的核心在于:如何判断网线"够不够"?以及如果够,我们需要动多少次手?

力扣 1319. 连通网络的操作次数

https://leetcode.cn/problems/number-of-operations-to-make-network-connected/

题目分析:

  • 输入n 台电脑(节点),connections 网线列表(边)。

  • 操作:拔掉一根已有的网线,把它连到另外两台电脑之间。

  • 目标:让所有电脑都连通。求最少操作次数。

  • 特殊情况 :如果线缆总数太少,无论怎么移都连不通,返回 -1

核心洞察:"硬性门槛"与"连通数学"

第一道门槛:资源够不够? 图论告诉我们:要连通 n 个节点,至少 需要 n - 1 条边(这就构成了一棵生成树)。

  • 如果 connections.size() < n - 1:线缆总数根本不够,神仙难救,直接返回 -1

  • 如果 connections.size() >= n - 1:线缆总数足够!既然总数够,我们一定能通过"拆除冗余、填补空缺"的方式,把图变成连通的。

第二道门槛:需要动几次手? 假设我们用并查集处理完所有的现有连接后,发现网络中还剩下 k互不相通 的连通分量(孤岛)。 要把这 k 个孤岛连成一个整体,我们需要搭几座桥? 很简单,把 k 个点串起来,需要 k - 1 条线。

结论: 只要总线缆数达标,我们只需要计算出当前的连通分量个数 count ,答案就是 count - 1。我们不需要关心具体拔哪根线,因为题目只问"次数"。

算法流程:并查集的标准作业

  1. 硬性检查 :如果 connections.size() < n - 1,返回 -1

  2. 初始化并查集

    • parent 数组,大小为 n

    • 关键 :初始化连通分量计数器 componentCount = n(一开始大家都是孤岛)。

  3. 遍历连接

    • 对于每一条边 [u, v],调用 union(u, v)

    • union 函数内部 :如果 uv 原本不属于同一个集合(rootU != rootV),合并它们,并将 componentCount 减 1

  4. 计算结果

    • 遍历结束后,componentCount 就是剩下的连通分量个数。

    • 需要的操作次数 = componentCount - 1

代码实现 (并查集模板应用)

C++

复制代码
#include <vector>
#include <numeric>

using namespace std;

class Solution {
private:
    // --- 并查集内部类 ---
    class UnionFind {
    private:
        vector<int> parent;
        int count; // 连通分量个数

    public:
        UnionFind(int n) {
            count = n;
            parent.resize(n);
            iota(parent.begin(), parent.end(), 0); // 初始化 0, 1, 2...
        }

        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]); // 路径压缩
            }
            return parent[x];
        }

        void unite(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX != rootY) {
                parent[rootX] = rootY;
                count--; // 每次成功合并,孤岛少一个
            }
        }

        int getCount() const {
            return count;
        }
    };

public:
    int makeConnected(int n, vector<vector<int>>& connections) {
        // 1. 硬性门槛检查:线缆总数是否足够
        if (connections.size() < n - 1) {
            return -1;
        }

        // 2. 初始化并查集
        UnionFind uf(n);

        // 3. 建立现有的连接
        for (const auto& conn : connections) {
            uf.unite(conn[0], conn[1]);
        }

        // 4. 计算需要的操作数
        // 剩下的连通分量个数 - 1,就是连接它们所需的边数
        return uf.getCount() - 1;
    }
};

深度复杂度分析

  • V (Vertices) :电脑数 n

  • E (Edges) :线缆数 connections.size()

  • 时间复杂度 O(n + E * α(n))

    • 初始化并查集需要 O(n)。

    • 遍历 E 条边进行合并,每次操作近似 O(1)(阿克曼反函数)。

    • 总时间近似线性。

  • 空间复杂度 O(n)

    • 并查集的 parent 数组。

总结:冗余与连通的辩证关系

今天这道题,让我们看到了图论中**"数量关系"**的美妙。

  • 冗余边:在一个已经连通的分量内部再加边,就是冗余(这就形成了环)。

  • 连通代价 :要把 k 个分量连通,必须且只需 k-1 条边。

只要我们手中的总边数 (资源)大于等于总节点数-1 (需求),我们就拥有了足够的"冗余边"去填补所有的"连通代价"。并查集在这里,不仅帮我们维护了连通性,通过维护 count 变量,它还直接告诉了我们当前的"分裂程度"。

下一篇,我们将利用并查集处理一种更有趣的逻辑关系------"等式方程的可满足性" 。当 ==!= 混杂在一起时,我们该如何判断逻辑是否冲突?

下期见!

相关推荐
小龙报6 小时前
【51单片机】从 0 到 1 玩转 51 蜂鸣器:分清有源无源,轻松驱动它奏响新年旋律
c语言·数据结构·c++·stm32·单片机·嵌入式硬件·51单片机
dllxhcjla6 小时前
数据结构和算法
数据结构
乌萨奇也要立志学C++6 小时前
【洛谷】BFS 求解最短路:从马的遍历到迷宫问题的实战解析
算法·宽度优先
老鼠只爱大米6 小时前
LeetCode经典算法面试题 #46:全排列(回溯、交换、剪枝等五种实现方案详细解析)
算法·leetcode·剪枝·回溯·全排列·stj算法
Dovis(誓平步青云)6 小时前
《滑动窗口算法:从 “暴力遍历” 到 “线性高效” 的思维跃迁》
运维·服务器·数据库·算法
_OP_CHEN7 小时前
【算法基础篇】(五十七)线性代数之矩阵乘法从入门到实战:手撕模板 + 真题详解
线性代数·算法·矩阵·蓝桥杯·c/c++·矩阵乘法·acm/icpc
天天爱吃肉82187 小时前
【跨界封神|周杰伦×王传福(陶晶莹主持):音乐创作与新能源NVH测试,底层逻辑竟完全同源!(新人必看入行指南)】
python·嵌入式硬件·算法·汽车
im_AMBER7 小时前
Leetcode 114 链表中的下一个更大节点 | 删除排序链表中的重复元素 II
算法·leetcode
xhbaitxl7 小时前
算法学习day38-动态规划
学习·算法·动态规划
多恩Stone7 小时前
【3D AICG 系列-6】OmniPart 训练流程梳理
人工智能·pytorch·算法·3d·aigc