算法与数据结构---并查集(Union-Find)

算法与数据结构---并查集(Union-Find)

并查集(Union-Find)是一种高效管理动态集合的数据结构,核心解决两个问题:「查询两个元素是否属于同一集合」和「合并两个集合」。它广泛应用于图论(如连通分量检测、最小生成树Kruskal算法)、社交网络(好友关系连通性)、集合分类等场景,其优化后的时间复杂度接近常数级,是算法面试中的高频考点。

一、并查集的核心概念

1. 基本定义

  • 元素与集合:每个元素初始时属于一个独立的集合(仅包含自身),集合用「根节点」标识(根节点的父节点是自身)。
  • 核心操作
    1. find(x):查询元素x所在集合的根节点(判断两个元素是否同集,只需比较根节点是否相同)。
    2. union(x, y):将元素xy所在的两个集合合并为一个集合。
    3. init(n):初始化n个独立集合(父节点数组、秩/大小数组初始化)。
  • 设计思想:用「树结构」表示集合(每个集合是一棵多叉树),根节点是集合的唯一标识,通过父节点指针串联所有元素。

二、朴素并查集实现(无优化)

1. 数据结构设计

用两个数组存储核心信息(C++中常用vector实现动态大小):

  • parent[]parent[i]表示元素i的父节点,初始时parent[i] = i(自环为根)。

2. 核心操作实现

(1)初始化
#include<vector>usingnamespace std;classUnionFind{private: vector<int> parent;// 父节点数组public:// 初始化n个独立集合(元素编号0~n-1)UnionFind(int n){ parent.resize(n);for(int i =0; i < n;++i){ parent[i]= i;// 每个元素的父节点是自身}}};
(2)find操作(递归版)

递归查找根节点,直到找到parent[x] == x的节点:

intfind(int x){if(parent[x]!= x){// 不是根节点,递归查找父节点returnfind(parent[x]);}return x;// 返回根节点}
(3)union操作(朴素版)

找到两个元素的根节点,将其中一个根节点的父节点指向另一个根节点:

voidunite(int x,int y){int rootX =find(x);int rootY =find(y);if(rootX != rootY){// 不同集合才合并 parent[rootY]= rootX;// 把rootY挂到rootX上}}

3. 朴素版本的问题

  • find操作效率低:树可能退化为链表(如依次合并1→2→3→4→…),此时find(x)的时间复杂度为O(n)
  • union操作无策略:随机合并导致树的高度激增,进一步恶化查询效率。

例如,若合并顺序为unite(0,1)、unite(1,2)、unite(2,3)...,树会变成一条长链,查询末尾元素的根节点需遍历所有节点。

三、并查集的两大优化

为将时间复杂度优化到近似O(α(n))α是阿克曼函数的反函数,n为元素个数时α(n)≤5,可视为常数),需实现以下两个优化:

1. 优化一:路径压缩(Path Compression)

原理

find(x)查询时,将路径上所有节点的父节点直接指向根节点,扁平化树结构,减少后续查询的路径长度。

实现(循环版,避免递归栈溢出)
intfind(int x){// 找到根节点int root = x;while(parent[root]!= root){ root = parent[root];}// 路径压缩:将x到根节点的所有节点直接挂到根上while(parent[x]!= root){int next = parent[x];// 保存x的原父节点 parent[x]= root;// x直接指向根 x = next;// 处理下一个节点}return root;}
效果

一次find(x)后,后续查询该路径上的任何节点,都能直接找到根节点,查询效率大幅提升。

2. 优化二:按秩合并(Union by Rank)/ 按大小合并(Union by Size)

原理

合并两个集合时,让“矮树”挂到“高树”的根上(按秩合并)或“小树”挂到“大树”的根上(按大小合并),避免树的高度无意义增长。

  • 秩(Rank):表示树的高度上限(初始时每个节点的秩为1)。
  • 大小(Size):表示集合的元素个数(初始时每个集合的大小为1)。
实现(按秩合并)

需新增rank[]数组存储每个根节点的秩:

classUnionFind{private: vector<int> parent; vector<int> rank;// 秩数组:根节点的秩表示树的高度上限public:UnionFind(int n){ parent.resize(n); rank.resize(n,1);// 初始秩为1for(int i =0; i < n;++i){ parent[i]= i;}}// 带路径压缩的findintfind(int x){if(parent[x]!= x){ parent[x]=find(parent[x]);// 递归版路径压缩(简洁,适合n较小场景)}return parent[x];}// 按秩合并voidunite(int x,int y){int rootX =find(x);int rootY =find(y);if(rootX == rootY)return;// 同集无需合并// 秩小的树挂到秩大的树上if(rank[rootX]< rank[rootY]){ parent[rootX]= rootY;}else{ parent[rootY]= rootX;// 若秩相等,合并后根的秩+1if(rank[rootX]== rank[rootY]){ rank[rootX]++;}}}};
实现(按大小合并)

需新增size[]数组存储每个集合的元素个数:

classUnionFind{private: vector<int> parent; vector<int> size;// 大小数组:根节点的size表示集合元素个数public:UnionFind(int n){ parent.resize(n); size.resize(n,1);// 初始大小为1for(int i =0; i < n;++i){ parent[i]= i;}}intfind(int x){// 循环版路径压缩(适合n较大,避免栈溢出)int root = x;while(parent[root]!= root) root = parent[root];while(parent[x]!= root){int next = parent[x]; parent[x]= root; x = next;}return root;}voidunite(int x,int y){int rootX =find(x);int rootY =find(y);if(rootX == rootY)return;// 小数组合并到大树(减少高度增长)if(size[rootX]< size[rootY]){ parent[rootX]= rootY; size[rootY]+= size[rootX];// 更新大树的大小}else{ parent[rootY]= rootX; size[rootX]+= size[rootY];}}};
选择建议
  • 按秩合并和按大小合并效果类似,均能保证树的高度不超过logn
  • 按大小合并更直观(可直接获取集合元素个数),按秩合并在某些场景(如带权并查集)更灵活。

四、并查集的核心功能扩展

除了基础的findunite,实际应用中常需以下扩展功能:

1. 查询集合元素个数

基于按大小合并的size[]数组,查询元素x所在集合的大小:

intgetSize(int x){int root =find(x);return size[root];}

2. 判断两个元素是否连通

boolisConnected(int x,int y){returnfind(x)==find(y);}

3. 统计连通分量个数

遍历所有元素,统计根节点的数量(parent[i] == i的节点数):

intcountComponents(){int cnt =0;for(int i =0; i < parent.size();++i){if(parent[i]== i){ cnt++;}}return cnt;}

五、并查集的经典应用场景(附C++示例)

场景1:无向图的环检测

问题描述

给定一个无向图,判断图中是否存在环。

原理

遍历所有边(u, v)

  • uv已连通(find(u) == find(v)),则添加该边会形成环。
  • 若未连通,则合并uv所在集合。
C++实现
#include<vector>#include<iostream>usingnamespace std;classUnionFind{private: vector<int> parent; vector<int> size;public:UnionFind(int n){ parent.resize(n); size.resize(n,1);for(int i =0; i < n;++i) parent[i]= i;}intfind(int x){int root = x;while(parent[root]!= root) root = parent[root];while(parent[x]!= root){int next = parent[x]; parent[x]= root; x = next;}return root;}voidunite(int x,int y){int rootX =find(x), rootY =find(y);if(rootX == rootY)return;if(size[rootX]< size[rootY]){ parent[rootX]= rootY; size[rootY]+= size[rootX];}else{ parent[rootY]= rootX; size[rootX]+= size[rootY];}}boolisConnected(int x,int y){returnfind(x)==find(y);}};// 判断无向图是否有环(节点数n,边集edges)boolhasCycle(int n, vector<vector<int>>& edges){ UnionFind uf(n);for(auto& edge : edges){int u = edge[0], v = edge[1];if(uf.isConnected(u, v)){returntrue;// 存在环} uf.unite(u, v);}returnfalse;}intmain(){int n =5; vector<vector<int>> edges ={{0,1},{1,2},{2,3},{3,1}};// 存在环 cout <<"是否有环:"<<(hasCycle(n, edges)?"是":"否")<< endl;// 输出:是return0;}

场景2:连通分量计数(岛屿数量变种)

问题描述

给定一个m×n的网格,1表示陆地,0表示水域,统计连通的陆地块数(上下左右为连通)。

原理

将每个陆地格子视为一个元素,编号为i×n + j(二维转一维),遍历每个陆地格子,与相邻陆地格子合并,最后统计连通分量个数。

C++实现
#include<vector>#include<iostream>usingnamespace std;classUnionFind{private: vector<int> parent; vector<int> size;int count;// 连通分量个数public:UnionFind(int n){ parent.resize(n); size.resize(n,1); count = n;// 初始每个元素是一个分量for(int i =0; i < n;++i) parent[i]= i;}intfind(int x){if(parent[x]!= x) parent[x]=find(parent[x]);return parent[x];}voidunite(int x,int y){int rootX =find(x), rootY =find(y);if(rootX == rootY)return;if(size[rootX]< size[rootY]){ parent[rootX]= rootY; size[rootY]+= size[rootX];}else{ parent[rootY]= rootX; size[rootX]+= size[rootY];} count--;// 合并后分量数-1}intgetCount(){return count;}};intnumIslands(vector<vector<char>>& grid){if(grid.empty()|| grid[0].empty())return0;int m = grid.size(), n = grid[0].size();int waterCount =0;// 水域数量(无需参与合并) UnionFind uf(m * n);// 方向数组:上下左右int dirs[4][2]={{-1,0},{1,0},{0,-1},{0,1}};for(int i =0; i < m;++i){for(int j =0; j < n;++j){if(grid[i][j]=='0'){ waterCount++;continue;}// 遍历相邻格子for(auto& dir : dirs){int x = i + dir[0], y = j + dir[1];if(x >=0&& x < m && y >=0&& y < n && grid[x][y]=='1'){int idx1 = i * n + j;int idx2 = x * n + y; uf.unite(idx1, idx2);}}}}// 总分量数 = 陆地分量数 = 总分量数 - 水域数return uf.getCount()- waterCount;}intmain(){ vector<vector<char>> grid ={{'1','1','0','0','0'},{'1','1','0','0','0'},{'0','0','1','0','0'},{'0','0','0','1','1'}}; cout <<"岛屿数量:"<<numIslands(grid)<< endl;// 输出:3return0;}

场景3:Kruskal算法求最小生成树

原理

Kruskal算法的核心是“选边不形成环”,利用并查集快速判断边的两个端点是否连通,优先选择权值最小的边合并,最终得到最小生成树。

六、带权并查集与扩展域并查集

1. 带权并查集(Weighted Union-Find)

核心思想

parent[]基础上,新增weight[]数组,存储节点到父节点的权值(如距离、偏移量、关系系数)。find(x)时不仅压缩路径,还需更新权值(维护路径上的权值累加);unite(x, y)时需根据已知关系计算根节点间的权值,确保权值一致性。

应用场景
  • 维护节点间的相对关系(如“x比y大5”“x是y的前驱”)。
  • 解决动态连通性中的权值传递问题(如LeetCode 399. 除法求值)。
C++简化示例(维护节点到根的距离)
classWeightedUnionFind{private: vector<int> parent; vector<int> weight;// weight[x]:x到parent[x]的距离public:WeightedUnionFind(int n){ parent.resize(n); weight.resize(n,0);// 初始时x到自身距离为0for(int i =0; i < n;++i) parent[i]= i;}intfind(int x){if(parent[x]!= x){int origParent = parent[x];// 保存原父节点 parent[x]=find(parent[x]);// 递归压缩路径(先找到根) weight[x]+= weight[origParent];// 更新x到根的距离(累加原父节点到根的距离)}return parent[x];}// 合并x和y,已知x到y的距离为d(即x = y + d)voidunite(int x,int y,int d){int rootX =find(x), rootY =find(y);if(rootX == rootY)return; parent[rootX]= rootY;// 计算rootX到rootY的距离:weight[x] + (rootX到rootY的距离) = d + weight[y] weight[rootX]= d + weight[y]- weight[x];}// 查询x到y的距离(若不连通返回-1)intgetDistance(int x,int y){if(find(x)!=find(y))return-1;return weight[x]- weight[y];}};

2. 扩展域并查集(Extended Union-Find)

核心思想

将每个元素的“状态”扩展为多个节点(如“x的朋友”“x的敌人”),通过合并扩展节点间接维护元素间的复杂关系(如“敌人的敌人是朋友”)。

应用场景
  • 处理对立关系(如LeetCode 886. 可能的二分法)。
  • 解决集合中的“黑白染色”问题。
核心思路
  • 每个元素x拆分为两个节点:x(表示“x属于A组”)和x + n(表示“x属于B组”)。
  • xy是敌人,则合并xy + nx + ny(x在A则y在B,x在B则y在A)。
  • xy是朋友,则合并xyx + ny + n(x和y同组)。

七、时间复杂度与空间复杂度分析

1. 时间复杂度

  • 朴素版:findunite均为O(n)(树退化为链表)。
  • 优化版(路径压缩+按秩/大小合并):单次操作时间复杂度为O(α(n))α是阿克曼函数的反函数,增长极慢(n≤1e6α(n)=4),可视为O(1)
  • 带权/扩展域并查集:时间复杂度与优化版一致,仅增加常数级的权值计算/扩展节点操作。

2. 空间复杂度

  • 基础版:O(n)parentrank/size数组)。
  • 带权版:O(n)(新增weight数组)。
  • 扩展域版:O(kn)k为扩展状态数,如敌人/朋友场景k=2)。

注意事项

1. 核心要点

  • 并查集的灵魂是「路径压缩」和「按秩/大小合并」,缺一不可,否则效率会急剧下降。
  • 优先使用循环版find(避免递归栈溢出,尤其当n较大时)。
  • 二维网格问题需将二维坐标转为一维编号(如i×n + j),方便并查集管理。

2. 常见误区

  • 合并时直接操作元素xy,而非它们的根节点(会导致合并失效)。
  • 路径压缩时忘记更新带权并查集的权值(导致权值错误)。
  • 扩展域并查集的状态拆分逻辑错误(如敌人关系合并方向颠倒)。

3. 适用场景总结

  • 动态连通性查询(如好友关系、网络连通)。
  • 集合合并与计数(如连通分量、岛屿数量)。
  • 无向图环检测(如Kruskal算法)。
  • 节点间关系维护(带权/扩展域并查集)。

Read more

Flutter 组件 calendar_time 的适配 鸿蒙Harmony 深度进阶 - 驾驭时间段语义隔离、实现鸿蒙端动态工作日排除与高并发列表动态刷新方案

Flutter 组件 calendar_time 的适配 鸿蒙Harmony 深度进阶 - 驾驭时间段语义隔离、实现鸿蒙端动态工作日排除与高并发列表动态刷新方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 calendar_time 的适配 鸿蒙Harmony 深度进阶 - 驾驭时间段语义隔离、实现鸿蒙端动态工作日排除与高并发列表动态刷新方案 前言 在前文中,我们利用 calendar_time 实现了基础的相对时间(如“刚才”、“昨天”)展示。但在真正的“金融级对账系统”、“政务排班大盘”或“高频社交动态”场景中。简单的相对描述远远不够。面对需要根据“当前业务时间”判定是否属于“法定工作时间”、针对包含上万条消息的列表如何实现高效的“每秒分钟数自增更新”。 如果处理不当,不仅会产生业务逻辑上的“时差错觉”。更会在鸿蒙(OpenHarmony)端引发严重的渲染性能雪崩。 本文将作为 calendar_time 适配的进阶篇。带你深入探讨其在鸿蒙端的逻辑时序对其、复杂区间判别(

By Ne0inhk

WSL needs updatingYour version of Windows Subsystem for Linux (WSL) is too old.如何解决

安装 Docker Desktop 时出现该问题,核心原因是:Docker Desktop 运行依赖 Windows Subsystem for Linux (WSL) 2 提供的轻量级虚拟化环境,而你的系统当前的 WSL 环境不符合运行要求。具体诱因主要有这三点: 1. WSL 功能未安装 / 版本过低:系统未启用 WSL 功能,或仅安装了旧版 WSL 1(Docker Desktop 硬性要求为 WSL 2 版本); 2. WSL 2 内核未更新:即便已安装 WSL 2,其内核组件未升级至最新版本,无法适配 Docker 运行需求; 3. 系统虚拟化功能未开启:Windows 未启用

By Ne0inhk
Flutter 组件 native_shuttle 的适配 鸿蒙Harmony 实战 - 驾驭极致原生通讯性能、实现鸿蒙端 Dart 与 ArkTS 之间的高频底层穿梭方案

Flutter 组件 native_shuttle 的适配 鸿蒙Harmony 实战 - 驾驭极致原生通讯性能、实现鸿蒙端 Dart 与 ArkTS 之间的高频底层穿梭方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 native_shuttle 的适配 鸿蒙Harmony 实战 - 驾驭极致原生通讯性能、实现鸿蒙端 Dart 与 ArkTS 之间的高频底层穿梭方案 前言 在鸿蒙(OpenHarmony)生态的极速动态图形交互、需要频繁调用系统底层多媒体能力以及对跨引擎数据同步有“毫秒级延时门禁”的各类专业级应用开发中,“宿主语言(ArkTS)与业务语言(Dart)之间的交互效率”是决定应用能否在大规模、高并发工况下保持流畅的终极技术壁垒。面对需要每秒传递 60 帧以上的高精度传感器数据流、复杂的 0307 批次资产二进制 Blob 同步或者是在鸿蒙平板与折叠屏之间执行频繁的逻辑状态投影。如果仅仅依靠传统的 MethodChannel 这种基于 JSON 或序列化编码的慢速异步通道。不仅会导致在数据转换(Serialization)过程中产生巨大的 CPU

By Ne0inhk

Flutter 三方库 persistent_cache_simple 的鸿蒙化适配指南 - 实现具备磁盘溢出淘汰与极简 API 的本地持久化缓存、支持端侧资源异步落地与状态秒开实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 persistent_cache_simple 的鸿蒙化适配指南 - 实现具备磁盘溢出淘汰与极简 API 的本地持久化缓存、支持端侧资源异步落地与状态秒开实战 前言 在进行 Flutter for OpenHarmony 应用开发时,如何高效、持久地缓存一些网络 JSON、配置片段或临时计算结果?传统的 shared_preferences 在处理大段字符串时性能受限,且缺乏生命周期淘汰机制。persistent_cache_simple 是一款功能专一、基于文件系统的轻量级缓存库。本文将探讨如何在鸿蒙端构建极致、稳健的二级缓存体系。 一、原直观解析 / 概念介绍 1.1 基础原理 该库建立在“键值映射至文件(Key-to-File)”的简易架构之上。它利用鸿蒙应用的沙箱存储目录,将每一个缓存项序列化为独立的文件。

By Ne0inhk