图论理论基础(2)

文章目录

  • 题型:并查集(Union-Find)
    • [1. 核心思路](#1. 核心思路)
      • [1.1 适用场景](#1.1 适用场景)
      • [1.2 核心思想](#1.2 核心思想)
      • [1.3 优化策略:路径压缩](#1.3 优化策略:路径压缩)
    • [2. 模板](#2. 模板)
      • [2.1 简化版本(函数式)](#2.1 简化版本(函数式))
      • [2.2 时间复杂度](#2.2 时间复杂度)
    • [3. 常见变形](#3. 常见变形)
      • [3.1 统计连通分量数量](#3.1 统计连通分量数量)
      • [3.2 按秩合并(可选优化)](#3.2 按秩合并(可选优化))
      • [3.3 带权并查集](#3.3 带权并查集)
    • [4. 典型应用场景](#4. 典型应用场景)
    • [5. 并查集 vs DFS/BFS](#5. 并查集 vs DFS/BFS)

题型:并查集(Union-Find)

1. 核心思路

并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题

1.1 适用场景

  • 判断两个元素是否在同一个集合中
  • 将两个元素合并到同一个集合中
  • 统计连通分量的数量

1.2 核心思想

  • 每个集合用一棵树表示,树的根节点作为该集合的代表
  • 通过查找根节点来判断两个元素是否属于同一集合
  • 通过合并根节点来合并两个集合

示例

复制代码
初始状态:每个元素都是独立的集合
A, B, C, D, E

合并A和B:   合并C和D:
  A            C
  |            |
  B            D

查找过程:A和B的根都是A → 在同一集合
         C和D的根都是C → 在同一集合
         E的根是E → 独立集合

1.3 优化策略:路径压缩

  • 问题:树可能退化成链,查找根节点需要O(n)时间
  • 解决:在查找过程中,将所有节点直接连接到根节点
  • 效果:第一次查找O(logn),后续查找接近O(1)

路径压缩示例

复制代码
压缩前(链状结构):       压缩后(扁平结构):
    A                        A
    |                      / | \
    B                     B  C  D
    |
    C
    |
    D

2. 模板

cpp 复制代码
class UnionFind {
private:
    vector<int> father;  // 父节点数组
    
public:
    // 初始化:每个节点的父节点都是自己
    UnionFind(int n) {
        father.resize(n);
        for (int i = 0; i < n; i++) {
            father[i] = i;
        }
    }
    
    // 查找根节点(带路径压缩)
    int find(int u) {
        // 如果u的父节点不是自己,说明u不是根节点
        // 递归查找根节点,并将路径上的所有节点直接连接到根节点
        return father[u] == u ? u : father[u] = find(father[u]);
    }
    
    // 判断两个节点是否在同一集合
    bool isSame(int u, int v) {
        return find(u) == find(v);
    }
    
    // 合并两个节点所在的集合
    void join(int u, int v) {
        u = find(u);  // 找到u的根节点
        v = find(v);  // 找到v的根节点
        if (u == v) return;  // 如果根相同,说明已经在同一集合,直接返回
        father[v] = u;  // 将v的根节点指向u的根节点
    }
};

2.1 简化版本(函数式)

cpp 复制代码
int n = 1000;
vector<int> father(n);

// 初始化
void init() {
    for (int i = 0; i < n; i++) {
        father[i] = i;
    }
}

// 查找根节点(路径压缩)
int find(int u) {
    return father[u] == u ? u : father[u] = find(father[u]);
}

// 判断是否在同一集合
bool isSame(int u, int v) {
    return find(u) == find(v);
}

// 合并集合
void join(int u, int v) {
    u = find(u);
    v = find(v);
    if (u == v) return;
    father[v] = u;
}

2.2 时间复杂度

  • 初始化:O(n)
  • 查找(路径压缩后):接近O(1),实际为O(α(n)),α是阿克曼函数的反函数
  • 合并:接近O(1)
  • 总体:对于n次操作,时间复杂度接近O(n)

3. 常见变形

3.1 统计连通分量数量

cpp 复制代码
int countComponents(int n, vector<vector<int>>& edges) {
    UnionFind uf(n);
    for (auto& edge : edges) {
        uf.join(edge[0], edge[1]);
    }
    
    // 统计根节点数量(根节点的father[i] == i)
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (uf.find(i) == i) {
            count++;
        }
    }
    return count;
}

3.2 按秩合并(可选优化)

在路径压缩的基础上,还可以使用按秩合并来进一步优化:

cpp 复制代码
class UnionFind {
private:
    vector<int> father;
    vector<int> rank;  // 记录树的深度(秩)
    
public:
    UnionFind(int n) {
        father.resize(n);
        rank.resize(n, 0);
        for (int i = 0; i < n; i++) {
            father[i] = i;
        }
    }
    
    int find(int u) {
        return father[u] == u ? u : father[u] = find(father[u]);
    }
    
    void join(int u, int v) {
        u = find(u);
        v = find(v);
        if (u == v) return;
        
        // 按秩合并:将秩小的树合并到秩大的树下
        if (rank[u] < rank[v]) {
            father[u] = v;
        } else if (rank[u] > rank[v]) {
            father[v] = u;
        } else {
            father[v] = u;
            rank[u]++;  // 秩相同时,合并后深度+1
        }
    }
};

3.3 带权并查集

记录节点到根节点的距离或关系:

cpp 复制代码
class WeightedUnionFind {
private:
    vector<int> father;
    vector<int> weight;  // 记录到根节点的权重
    
public:
    WeightedUnionFind(int n) {
        father.resize(n);
        weight.resize(n, 0);
        for (int i = 0; i < n; i++) {
            father[i] = i;
        }
    }
    
    int find(int u) {
        if (father[u] != u) {
            int root = find(father[u]);
            weight[u] += weight[father[u]];  // 更新权重
            father[u] = root;
        }
        return father[u];
    }
    
    void join(int u, int v, int w) {
        int rootU = find(u);
        int rootV = find(v);
        if (rootU == rootV) return;
        father[rootV] = rootU;
        weight[rootV] = weight[u] - weight[v] + w;
    }
};

4. 典型应用场景

  1. 连通性问题

    • LeetCode 547. 省份数量
    • LeetCode 200. 岛屿数量(也可用DFS/BFS)
    • LeetCode 130. 被围绕的区域
  2. 朋友圈/社交网络

    • 判断两个人是否在同一个朋友圈
    • 统计朋友圈数量
  3. 最小生成树(Kruskal算法)

    • 使用并查集判断边是否会形成环
    • LeetCode 1584. 连接所有点的最小费用
  4. 等价关系判断

    • 判断两个变量是否等价
    • 字符串相似性判断
  5. 动态连通性

    • 实时添加边,判断连通性
    • 网络连接问题

5. 并查集 vs DFS/BFS

比较项 并查集 DFS/BFS
适用场景 动态合并集合、判断连通性 遍历图、查找路径
时间复杂度 接近O(1) O(V+E)
空间复杂度 O(V) O(V)
优势 合并和查询操作高效 可以遍历所有节点、找路径
劣势 不能遍历图结构 每次查询需要重新遍历

选择建议

  • 需要频繁合并集合、判断连通性 → 并查集
  • 需要遍历图、找路径、统计连通块 → DFS/BFS
相关推荐
L***d6701 小时前
Spring Boot 经典九设计模式全览
java·spring boot·设计模式
枫叶丹41 小时前
浙人医信创实践:电科金仓异构多活架构破解集团化医院转型难题
开发语言·数据库·架构
我命由我123451 小时前
Android 开发问题:布局文件中的文本,在预览时有显示出来,但是,在应用中没有显示出来
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Sheffi661 小时前
Objective-C 黑魔法:Method Swizzling 的正确姿势与滥用风险
开发语言·macos·objective-c
7***q6081 小时前
【保姆级教程】apache-tomcat的安装配置教程
java·tomcat·apache
点云SLAM1 小时前
四元数 (Quaternion)微分-单位四元数 q(t) 的导数详细推导(10)
算法·计算机视觉·机器人·slam·imu·四元数·单位四元数求导
秋邱1 小时前
2025 年突破性科技:大模型驱动的实时多模态数据流处理系统
人工智能·科技·算法·机器学习
sin_hielo1 小时前
leetcode 2141
数据结构·算法·leetcode
shayudiandian1 小时前
【Java】枚举类
java