前言
C++图论
C++算法与数据结构
本博文代码打包下载
基本概念
并查集(Union-Find)是一种用于处理动态连通性(直接或间接相连)的数据结构,主要支持两种操作:union 和 find。通过这两个基本操作,可以高效地管理一组元素之间的连通关系。
Find: 查找节点所在有向树的根。
Union: 将两个不同的有向图合并为一棵树。
暴力做法
并集查找处理无向图的数据结构:有向森林,每棵树都是内向树。连通子图都直接或间接指向根,根出度为0,其它节点出度为1。vPar记录各节点的父节点。
Find(u)函数寻找u所在有向树的根(最远祖先):
while(-1 != vPar[u]){ u =vPar}
return u;
判断u和v是否连通:
return Find(u)==Find(v)
连通:
root1 = Find(u);
root2 = Find(v);
如果root1和root2相等,直接返回。否则会有自环。
vPar[root1] = root2;
初始和合并都是有向森林
可以用数学归纳法证明。
初始:所有连通子图都是只有一个节点的有向树。
合并前后:
root1是根,出度0。合并后不是根,出度为1。
root2合并前后都是根,出度为0,不变。
其它节点合并前后都不是根,出度不变。
合并前后:v节点所在子图都直接或间接指向root2。
合并后:u所在子树通过u间接指向root2。
时间复杂度
合并和查找都是O(n)
启发式合并(UNION BY RANK)
合并前,u、v所在子树的节点数分别为mu、mv。
{
v
P
a
r
[
r
o
o
t
1
]
=
r
o
o
t
2
u
树所有节点层次
+
1
,
v
子树层次不变。
m
u
<
=
m
v
v
P
a
r
[
r
o
o
t
2
]
=
r
o
o
t
1
v
树所有节点层次
+
1
,
u
子树层次不变
其它
\begin{cases} vPar[root1]= root2 & u树所有节点层次+1,v子树层次不变。 & mu <= mv \\ vPar[root2]=root1 & v树所有节点层次+1,u子树层次不变& 其它 \end{cases}
{vPar[root1]=root2vPar[root2]=root1u树所有节点层次+1,v子树层次不变。v树所有节点层次+1,u子树层次不变mu<=mv其它
任意节点合并的次数不会超过
l
o
g
2
n
log2n
log2n,否则合并此树的节点数超过n。被合并一次层次数+1,故所有节点的层次不互已超过
l
o
g
2
n
log2n
log2n。
路径压缩(PATH COMPRESSION)
路径压缩的优点在于它的简单性和有效性。尽管单次 find 的时间复杂度可能较高,但由于摊还分析的结果表明,经过多次操作后,平均时间复杂度接近常数 ( O(\alpha(n)) ),其中 ( \alpha(n) ) 是反阿克曼函数,增长极其缓慢}
u及u所有的祖先都直接连通root2。
合并和查找都是O(log2n)
可撤销并集查找
以启发式合并为基础。每次连通:
vPar[root1]= root2。 我们记录root1、修改之前的x=vPar[root1]。
vCnt[root2] += vCnt[root1],我们记录root2,y=vCnt[root1]。
撤销时:
vPar[root1] = x vCnt[root2] -=y
用栈记录操作记录。已经连通也要有记录。
封装类代码
无向图的并集查找(路径压缩)
class CUnionFind
{
public:
CUnionFind(int iSize) :m_vNodeToRegion(iSize)
{
for (int i = 0; i < iSize; i++)
{
m_vNodeToRegion[i] = i;
}
m_iConnetRegionCount = iSize;
}
CUnionFind(vector<vector<int>>& vNeiBo):CUnionFind(vNeiBo.size())
{
for (int i = 0; i < vNeiBo.size(); i++) {
for (const auto& n : vNeiBo[i]) {
Union(i, n);
}
}
}
int GetConnectRegionIndex(int iNode)
{
int& iConnectNO = m_vNodeToRegion[iNode];
if (iNode == iConnectNO)
{
return iNode;
}
return iConnectNO = GetConnectRegionIndex(iConnectNO);
}
void Union(int iNode1, int iNode2)
{
const int iConnectNO1 = GetConnectRegionIndex(iNode1);
const int iConnectNO2 = GetConnectRegionIndex(iNode2);
if (iConnectNO1 == iConnectNO2)
{
return;
}
m_iConnetRegionCount--;
if (iConnectNO1 > iConnectNO2)
{
m_vNodeToRegion[iConnectNO1] = iConnectNO2;
}
else
{
m_vNodeToRegion[iConnectNO2] = iConnectNO1;
}
}
bool IsConnect(int iNode1, int iNode2)
{
return GetConnectRegionIndex(iNode1) == GetConnectRegionIndex(iNode2);
}
int GetConnetRegionCount()const
{
return m_iConnetRegionCount;
}
//vector<int> GetNodeCountOfRegion()//各联通区域的节点数量
//{
// const int iNodeSize = m_vNodeToRegion.size();
// vector<int> vRet(iNodeSize);
// for (int i = 0; i < iNodeSize; i++)
// {
// vRet[GetConnectRegionIndex(i)]++;
// }
// return vRet;
//}
std::unordered_map<int, vector<int>> GetNodeOfRegion()
{
std::unordered_map<int, vector<int>> ret;
const int iNodeSize = m_vNodeToRegion.size();
for (int i = 0; i < iNodeSize; i++)
{
ret[GetConnectRegionIndex(i)].emplace_back(i);
}
return ret;
}
private:
vector<int> m_vNodeToRegion;//各点所在联通区域的索引,本联通区域任意一点的索引,为了增加可理解性,用最小索引
int m_iConnetRegionCount;
};
实时更新各连通区域节点的并集查找
//启发式合并,实时更新各连通区域的节点
class CUnionFindNodes : public CUnionFind {
public:
CUnionFindNodes(int n) :CUnionFind(n) {
m_nodes.resize(n);
for (int i = 0; i < n; i++) {
m_nodes[i].emplace_back(i);
}
}
void Union(int iNode1, int iNode2)
{
int g0 = GetConnectRegionIndex(iNode1);
int g1 = GetConnectRegionIndex(iNode2);
if (g0 == g1) { return; }
CUnionFind::Union(iNode1, iNode2);
if (g1 == GetConnectRegionIndex(g0)) {
swap(g0, g1);
}
if (m_nodes[g1].size() > m_nodes[g0].size()) {
swap(m_nodes[g1], m_nodes[g0]);
}
m_nodes[g0].insert(m_nodes[g0].end(), m_nodes[g1].begin(), m_nodes[g1].end());
m_nodes[g1].clear();
}
vector<vector<int>> m_nodes;
};
无向图的并集查找
如果一条会形成环或出度为2,忽略。
class CUnionFindDirect
{
public:
CUnionFindDirect(int iSize)
{
m_vRoot.resize(iSize);
iota(m_vRoot.begin(), m_vRoot.end(), 0);
}
void Connect(bool& bConflic, bool& bCyc, int iFrom, int iTo)
{
bConflic = bCyc = false;
if (iFrom != m_vRoot[iFrom])
{
bConflic = true;
}
Fresh(iTo);
if (m_vRoot[iTo] == iFrom)
{
bCyc = true;
}
if (bConflic || bCyc)
{
return;
}
m_vRoot[iFrom] = m_vRoot[iTo];
}
int GetMaxDest(int iFrom)
{
Fresh(iFrom);
return m_vRoot[iFrom];
}
private:
int Fresh(int iNode)
{
if (m_vRoot[iNode] == iNode)
{
return iNode;
}
return m_vRoot[iNode] = Fresh(m_vRoot[iNode]);
}
vector<int> m_vRoot;
};
可撤销的并集查找
class CUnDoUnionFind {
public:
CUnDoUnionFind(int N) :m_par(N, -1), m_cnt(N, 1) {
};
int Find(int x) {
while (-1 != m_par[x]) { x = m_par[x]; }
return x;
}
void Union(int u, int v) {
int root1 = Find(u);
int root2 = Find(v);
if (root1 == root2) {
m_record.emplace(root1, m_par[root1], root2, 0);
return;
}
if (m_cnt[root1] > m_cnt[root2]) {
swap(u, v);
swap(root1, root2);
}
m_record.emplace(root1, m_par[root1], root2, m_cnt[root1]);
m_par[root1] = root2;
m_cnt[root2] += m_cnt[root1];
}
bool Undo() {
if (m_record.empty()) { return false; }
const auto [root1, par, root2, cnt] = m_record.top(); m_record.pop();
m_par[root1] = par;
m_cnt[root2] -= cnt;
return true;
}
vector<int> m_par, m_cnt;
stack<tuple<int, int, int, int>> m_record;
};
样例
力扣
难度分 | |
---|---|
【欧拉回路】【图论】【并集查找】765. 情侣牵手 | 无 |
【C++图论 并集查找】2316. 统计无向图中无法互相到达点对数 | 1604 |
【C++图论 并集查找】1319. 连通网络的操作次数 | 1633 |
【C++图论 并集查找】2492. 两个城市间路径的最小分数 | 1679 |
【C++并集查找 图论】1202. 交换字符串中的元素 | 1855 |
【C++图论 并集查找】924. 尽量减少恶意软件的传播 | 1868 |
【图论】【并集查找】【C++算法】928. 尽量减少恶意软件的传播 II | 1985 |
【C++ 并集查找】1722. 执行交换操作后的最小汉明距离 | 1892 |
【图论】【广度优先】【 并集查找】2092 找出知晓秘密的所有专家 | 2003 |
【C++图论 并集查找】947. 移除最多的同行或同列石头 | 2034 |
C++二分查找或并集查找:2948交换得到字典序最小的数组 | 2047 |
【并集查找】839. 相似字符串组 | 2053 |
【C++ 同余 裴蜀定理 中位数贪心 并集查找】2607. 使子数组元素和相等 | 2071 |
【并集查找 图论 位运算】3108. 带权图里旅途的最小代价 | 2108 |
【C++图论 并集查找】1579. 保证图可完全遍历 | 2131 |
【最大公约 调和级数 并集查找】2709. 最大公约数遍历 | 2171 |
【调和级数 并集查找】1627. 带阈值的图连通性 | 2221 |
【并集查找 最大公约数 调和数】952. 按公因数计算最大组件大小 | 2272 |
【并集查找】749. 隔离病毒 | 2277 |
【并集查找 离线查询】1697. 检查边长度限制的路径是否存在 | 2300 |
【广度优先搜索】【二分图】【并集查找】2493. 将节点分成尽可能多的组 | 2415 |
【最大公约数 并集查找 调和级数】1998. 数组的最大公因数排序 | 2429 |
【并集查找 图论】2421. 好路径的数目 | 2444 |
【状态压缩 并集查找 图论】2157. 字符串分组 | 2499 |
【图轮】【 最小生成树】【 并集查找】1489. 找到最小生成树里的关键边和伪关键边 | 2571 |
洛谷普及+
【二分查找 并集查找】P6004 [USACO20JAN] Wormhole Sort S | 普及+ |
【图论 BFS染色 并集查找 】P3663 [USACO17FEB] Why Did the Cow Cross the Road III S | 普及+ |
【图论 并集查找】P3671 [USACO17OPEN] Where‘s Bessie? S | 普及+ |
【拓扑排序 并集查找】P8269 [USACO22OPEN] Visits S | 普及+ |
【贪心 逆向思考 并集查找 数学归纳法】P7162 [COCI 2020/2021 #2] Sjekira | 普及+ |
【并集查找】P7299 [USACO21JAN] Dance Mooves S | 普及+ |
【并集查找 逆向思考】P8097 [USACO22JAN] Farm Updates G | 普及+ |
【位运算 并集查找】P7973 [KSN2021] Binary Land | 普及+ |
【并集查找】P7991 [USACO21DEC] Connecting Two Barns S | 普及+ |
【并集查找 最小生成树】P8191 [USACO22FEB] Moo Network G | 普及+ |
【并集查找 启发性合并】P8710 [蓝桥杯 2020 省 AB1] 网络分析 | 普及+ |
【并集查找 有向图 逆序思考】P8359 [SNOI2022] 垃圾回收 | 普及+ |
【并集查找】8210 [THUPC 2022 初赛] 造计算机 | 普及+ |
【并集查找 拓扑序】P9013 [USACO23JAN] Find and Replace S | 普及+ |
【最短路 并集查找】P9327 [CCC 2023 S4] Minimum Cost Roads | 普及+ |
【二分图 扩展域并集查找】P1525 [NOIP 2010 提高组] 关押罪犯 | 普及+ |
【二分图 扩展域并集查找】P11409 西湖有雅座 | 普及+ |
【单调栈 双连通 并集查找】P12169 [蓝桥杯 2025 省 C/Python A/Java C] 登山 | 普及+ |
【并集查找】P12274 [蓝桥杯 2024 国 Python B] 设计 | 普及+ |
样例整理时间
2025-5-25
投票
# 扩展阅读
我想对大家说的话 |
---|
工作中遇到的问题,可以按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。 |
学习算法:按章节学习《喜缺全书算法册》,大量的题目和测试用例,打包下载。重视操作 |
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注 |
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。 |
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。 |
如果程序是一条龙,那算法就是他的是睛 |
失败+反思=成功 成功+反思=成功 |
视频课程
先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176
测试环境
操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。