当前位置: 首页 > java >正文

【数据结构与算法】并查集

代码路径: 并查集 · zengjx/algorithm - 码云 - 开源中国

牛客网: 题解 | 【模板】最小生成树_牛客网

一、原始无向加权图(6 个节点:0-5)

1. 节点位置建议(便于画图)

为了清晰展示边的连接关系,建议按以下位置排列节点(类似 “两层分布”):

  • 上层(从左到右):节点 0、节点 1、节点 2
  • 下层(从左到右):节点 3、节点 4、节点 5
2. 边与权重标注(按原始数据)

用 “线段连接节点”,并在线段旁标注权重,所有边如下(无方向,双向连通):

plaintext

          (上层)[0] —4— [1]|       |  \3       8    2|       |  /[2] —1— [3] —2— [5]|       |       |7       |       6|       |       |[4] ————(无直接边,需通过5连接)(下层)
3. 完整边列表(对应图形)
节点对权重图形中的连接关系
0-14上层左 1(0)连上层左 2(1)
0-23上层左 1(0)连上层左 3(2)
0-35上层左 1(0)连下层左 1(3)
1-22上层左 2(1)连上层左 3(2)
1-38上层左 2(1)连下层左 1(3)
2-31上层左 3(2)连下层左 1(3)
2-47上层左 3(2)连下层左 2(4)
3-52下层左 1(3)连下层左 3(5)
4-56下层左 2(4)连下层左 3(5)

二、最小生成树(MST)结构

MST 仅保留 5 条边(6 个节点需 n-1=5 条边),总权重 14,边为:(2-3,1)、(1-2,2)、(3-5,2)、(0-2,3)、(4-5,6)。

1. MST 图形描述(保留关键边,去掉冗余边)

plaintext

          (上层)[0] —3— [2] —2— [1]/ |1  |/   |[3] —2— [5] —6— [4](下层)
2. MST 边的标注(加粗为保留边)
  • 核心连接:[2] 是 “枢纽节点”,连接 [0](权重 3)、[1](权重 2)、[3](权重 1);
  • 下层连接:[3] 连 [5](权重 2),[5] 连 [4](权重 6);
  • 去掉的边:0-1(4)、0-3(5)、1-3(8)、2-4(7)(这些边会形成环或权重更大)。

并查集

并查集介绍

并查集(Union-Find)是一种用于处理集合合并和元素查询的数据结构,主要支持两种操作:

  1. 查找(Find):确定某个元素属于哪个集合
  2. 合并(Union):将两个集合合并成一个集合

它在处理连通性问题时非常高效,比如判断图中两点是否连通、网络连接问题等场景。

并查集的基本实现

并查集通常用数组实现,每个元素有一个父节点,当父节点是自身时候,表示该元素是集合的根节点。

python代码 

class UnionFind:def __init__(self, size):# 初始化父节点数组,每个元素的父节点是自己self.parent = list(range(size))# 初始化秩(用于优化合并操作)self.rank = [0] * sizedef find(self, x):"""查找元素x所在集合的根节点"""# 路径压缩:将x的父节点直接指向根节点,加快后续查询if self.parent[x] != x:self.parent[x] = self.find(self.parent[x])return self.parent[x]def union(self, x, y):"""合并元素x和y所在的集合"""# 找到两个元素的根节点root_x = self.find(x)root_y = self.find(y)# 如果已经在同一个集合中,则不需要合并if root_x == root_y:return# 按秩合并:将秩较小的树合并到秩较大的树下面if self.rank[root_x] < self.rank[root_y]:self.parent[root_x] = root_yelse:self.parent[root_y] = root_x# 如果秩相等,合并后秩加1if self.rank[root_x] == self.rank[root_y]:self.rank[root_x] += 1def is_connected(self, x, y):"""判断元素x和y是否在同一个集合中"""return self.find(x) == self.find(y)

  

初始化:
parent数组:存储每个元素的父节点
rank数组:存储每个集合的秩(可以理解为树的高度)
查找操作:
使用递归实现路径压缩,将查询路径上的所有节点直接指向根节点
路径压缩大大加快了后续的查询速度
合并操作:
先找到两个元素的根节点
使用按秩合并策略,将较矮的树合并到较高的树下面
这样可以保持树的平衡性,避免出现极端的长链结构
连通性判断:
两个元素在同一个集合中当且仅当它们的根节点相同

时间复杂度

  • 带路径压缩和按秩合并的并查集,每次操作的时间复杂度接近 O (1)
  • 严格来说是 O (α(n)),其中 α 是阿克曼函数的反函数,增长极其缓慢,在实际应用中可视为常数

并查集是一种非常高效的数据结构,在处理动态连通性问题时表现出色,广泛应用于图论、网络分析、 Kruskal 算法等领域。

例 1:判断社交网络中的朋友关系

假设有 5 个人(编号 0-4),我们想通过并查集来管理他们之间的朋友关系,并判断任意两人是否是朋友(直接或间接):

python

运行

# 创建包含5个人的并查集
uf = UnionFind(5)# 建立朋友关系(合并集合)
uf.union(0, 1)  # 0和1成为朋友
uf.union(1, 2)  # 1和2成为朋友(0、1、2现在是一个朋友圈)
uf.union(3, 4)  # 3和4成为朋友# 判断关系
print(uf.is_connected(0, 2))  # True(0和2通过1间接是朋友)
print(uf.is_connected(0, 3))  # False(0和3不在同一个朋友圈)# 建立更多关系
uf.union(2, 3)  # 2和3成为朋友(现在所有人都在一个朋友圈)
print(uf.is_connected(0, 4))  # True(0和4现在间接是朋友)

例 2:网格中的连通分量计数

假设有一个 3x3 的网格,我们想计算其中连通的 1 的块数(上下左右相邻视为连通):

python

运行

grid = [[1, 1, 0],[0, 1, 0],[0, 0, 1]
]# 初始化并查集,每个1初始为独立集合
uf = UnionFind(9)  # 3x3网格共9个位置
count = sum(row.count(1) for row in grid)  # 初始连通分量数等于1的个数# 遍历网格,合并相邻的1
for i in range(3):for j in range(3):if grid[i][j] == 1:# 检查右侧元素if j < 2 and grid[i][j+1] == 1:uf.union(i*3+j, i*3+(j+1))count -= 1# 检查下方元素if i < 2 and grid[i+1][j] == 1:uf.union(i*3+j, (i+1)*3+j)count -= 1print(f"连通分量数: {count}")  # 输出: 2

count = sum(row.count(1) for row in grid) # 初始连通分量数等于1的个数  的解释

这行代码的作用是计算网格中值为1的元素总数,作为连通分量的初始计数。
让我们逐步解析:
row.count(1) for row in grid:
这是一个生成器表达式,遍历网格中的每一行(row in grid)
对于每一行,row.count(1) 计算当前行中值为1的元素个数
sum(...):
对生成器表达式的结果求和,得到整个网格中所有值为1的元素总数
为什么初始连通分量数等于 1 的个数?
在开始处理前,每个1都被视为一个独立的连通分量(因为还没有检查它们之间的相邻关系)
例如,如果网格中有 5 个1且它们互不相邻,那么就有 5 个连通分量
当我们通过并查集合并相邻的1时,每合并一次,连通分量数就减 1
举个例子:
如果网格是:
python
运行
grid = [[1, 1, 0],[0, 1, 0],[0, 0, 1]
]第一行有 2 个1
第二行有 1 个1
第三行有 1 个1
总和是 4,所以初始count = 4
随着后续合并相邻的1,这个计数会不断减少,最终得到实际的连通分量数量。

例 3:Kruskal 算法求最小生成树

并查集常用于 Kruskal 算法中,判断添加边是否会形成环:

一、先明确基础概念

  1. 最小生成树(MST):在包含 n 个节点的无向连通图中,选择 n-1 条边,使得:

    • 所有节点通过这些边连通(无孤立节点);
    • 所有边的权重之和 最小
    • 无环(否则边数会超过 n-1,权重和必然不是最小)。
  2. Kruskal 算法核心思想
    • 按 边的权重从小到大排序
    • 依次选择权重最小的边,若该边连接的两个节点 不在同一个连通分量(用并查集判断,避免形成环),则将该边加入 MST,并合并两个节点的连通分量;
    • 重复步骤 2,直到 MST 包含 n-1 条边(此时所有节点已连通)。

python

运行

def kruskal(n, edges):"""n: 节点数edges: 边的列表,每个元素为(权重, u, v)"""uf = UnionFind(n)edges.sort()  # 按权重排序total_weight = 0mst = []for weight, u, v in edges:# 如果两个节点不在同一集合,添加这条边不会形成环if not uf.is_connected(u, v):uf.union(u, v)total_weight += weightmst.append((u, v, weight))# 当添加了n-1条边时,最小生成树完成if len(mst) == n - 1:breakreturn mst, total_weight# 示例
n = 4  # 4个节点
edges = [(10, 0, 1),(6, 0, 2),(5, 0, 3),(15, 1, 3),(4, 2, 3)
]mst, weight = kruskal(n, edges)
print(f"最小生成树总权重: {weight}")  # 输出: 19

二、示例图与数据准备

1. 示例图结构

假设有一个 6 个节点(编号 0-5)的无向加权图,边的权重如下表所示(边的顺序无关,后续会排序):

边(起点 - 终点)权重边(起点 - 终点)权重边(起点 - 终点)权重
0-141-383-52
0-232-314-56
0-352-471-22
2. 初始状态
  • 节点数 n=6,因此 MST 需要 n-1=5 条边;
  • 初始时,每个节点都是独立的连通分量(用并查集表示:parent[0]=0, parent[1]=1, ..., parent[5]=5);
  • MST 为空,已选边数 count=0,总权重 total_weight=0

三、Kruskal 算法分步执行

步骤 1:按边的权重从小到大排序

首先将所有边按权重升序排列(权重相同的边顺序不影响最终结果):

  1. 2-3(权重 1)→ 2. 1-2(权重 2)→ 3. 3-5(权重 2)→ 4. 0-2(权重 3)→ 5. 0-1(权重 4)→ 6. 0-3(权重 5)→ 7. 4-5(权重 6)→ 8. 2-4(权重 7)→ 9. 1-3(权重 8)
步骤 2:依次选边,用并查集判断是否形成环

我们逐条处理排序后的边,核心逻辑是:选边不形成环则加入 MST,否则跳过

步骤待选边(u-v)权重并查集判断(u 和 v 是否同属一个连通分量)操作MST 当前边数MST 总权重连通分量变化(并查集合并后)
12-31u=2(根为 2),v=3(根为 3)→ 不同加入 MST,合并 2 和 311{0}, {1}, {2,3}, {4}, {5}
21-22u=1(根为 1),v=2(根为 2)→ 不同加入 MST,合并 1 和 221+2=3{0}, {1,2,3}, {4}, {5}
33-52u=3(根为 2),v=5(根为 5)→ 不同加入 MST,合并 3 和 533+2=5{0}, {1,2,3,5}, {4}
40-23u=0(根为 0),v=2(根为 2)→ 不同加入 MST,合并 0 和 245+3=8{0,1,2,3,5}, {4}
50-14u=0(根为 2),v=1(根为 2)→ 相同跳过(形成环)48无变化
60-35u=0(根为 2),v=3(根为 2)→ 相同跳过(形成环)48无变化
74-56u=4(根为 4),v=5(根为 2)→ 不同加入 MST,合并 4 和 558+6=14{0,1,2,3,4,5}(所有节点连通)
-后续边(2-4、1-3)-已选够 5 条边(n-1),算法终止停止处理---

四、最终结果

  • 最小生成树(MST)的边:(2-3,1)、(1-2,2)、(3-5,2)、(0-2,3)、(4-5,6)
  • MST 总权重:1+2+2+3+6 = 14
  • MST 结构:所有 6 个节点通过 5 条边连通,且无环、权重和最小。

五、关键总结:并查集的作用

在 Kruskal 算法中,并查集是判断 “是否形成环” 的核心工具

  • 每次选边前,通过 find(u) 和 find(v) 查看两个节点的根是否相同:
    • 若根相同:说明两个节点已在同一个连通分量中,加边会形成环,跳过;
    • 若根不同:说明加边后不会形成环,将边加入 MST,并通过 union(u,v) 合并两个连通分量。

通过 “按权重排序 + 并查集判环”,Kruskal 算法高效地找到了最小生成树,时间复杂度主要由 “边排序”(O (E log E),E 为边数)和 “并查集操作”(接近 O (1))决定,适用于边数较少的稀疏图。

"""并查集常用于Krushkal 算法中,判断添加边是够会形成环
"""
from 并查集 import UnionFinddef kruskal(n, edges):""":param n: 节点数:param edges: 边的列表,每个元素为(权重,u,v):return:"""uf = UnionFind(n)#edges.sort(edges)  #按照权重重排序# 按照权重排序sorted_edges = sorted(edges, key=lambda x: (x[2]))print("按照权重排序:",sorted_edges)total_weight = 0mst = []for  u, v,weight in sorted_edges:# 如果两个节点不在同一个集合,添加这条边不会形成环print(weight)if not uf.is_connected(u,v):uf.union(u, v)total_weight += weightmst.append((u, v, weight))# 当添加的n-1 条边时,最小生成树形成if len(mst) == n - 1:breakreturn mst, total_weightdef demo1():n = 4edges = [( 0, 1,10), (0,2,6), (0, 3,5), ( 1, 3,15), ( 2, 3,4)]mst, weight = kruskal(n, edges)print(mst, weight)def demo2():n = 6edges = [(0, 1, 4), (0, 2, 3), (0, 3, 5), (1, 3, 8), (2, 3, 1), (2, 4, 7), (3, 5, 2), (4, 5, 6), (1, 2, 2)]mst, weight = kruskal(n, edges)print("最小生成树(起点,终点,权重)",mst)print("最小生成树的权重",weight)if __name__ == '__main__':demo1()demo2()

    http://www.xdnf.cn/news/18700.html

    相关文章:

  • 当GitHub“断网”:从应急到终极方案,手把手搭建永不宕机的代码协作体系
  • LLM 中增量解码与模型推理解读
  • 包装类 抽象类 内部类 接口
  • Flink Slot 不足导致任务Pending修复方案
  • VirtualBox 中安装 Ubuntu 22.04
  • 基于Java、GeoTools与PostGIS的对跖点求解研究
  • 如何快速对接印度股票市场数据API?完整开发指南
  • Solidity学习笔记
  • MATLAB实现CNN-GRU-Attention时序和空间特征结合-融合注意力机制混合神经网络模型的风速预测
  • AI Agent全栈开发流程推荐(全栈开发步骤)
  • Kubernetes v1.34 前瞻:资源管理、安全与可观测性的全面进化
  • 【和春笋一起学C++】(三十五)类的使用实例
  • 1.Spring Boot:超越配置地狱,重塑Java开发体验
  • 逆光场景识别率↑76%!陌讯多模态融合算法在手机拍照识别的落地实践​
  • centos安装jenkins
  • 校园跑腿小程序源码 | 跑腿便利店小程序 含搭建教程
  • bun + vite7 的结合,孕育的 Robot Admin 【靓仔出道】(十八)
  • 目标检测数据集 第005期-基于yolo标注格式的PCB组件检测数据集(含免费分享)
  • JavaScript数据结构详解
  • 智元精灵GO1 agibot数据转换Lerobot通用格式数据脚本
  • [创业之路-567]:数字技术、数字产品、数字资产、数字货币、数字企业、数字经济、数字世界、数字人生、数字智能、数字生命
  • 大模型知识--Function Calls
  • element-plus穿梭框transfer的调整
  • 【实习总结】快速上手Git:关键命令整理
  • AI版权保护破局内容行业痛点:侵权识别效率升89%+维权周期缩至45天,区块链存证成关键
  • vue中 computed vs methods
  • unity热更新总结
  • Linux的线程概念与控制
  • CTFshow系列——命令执行web49-52
  • 基于深度学习的眼疾识别系统:从血细胞分类到病理性近视检测