本文用于个人算法竞赛学习,仅供参考
目录
一.什么是并查集
二.并查集实现
三.路径优化
四.时间复杂度
五.并查集+路径压缩 模板
五.题目
一.什么是并查集
并查集(Disjoint Set)是一种数据结构,用于处理一系列不相交的集合的合并与查询问题。并查集主要支持两种操作:
1. 合并(Union):将两个集合合并为一个集合。
2. 查询(Find):确定一个元素属于哪个集合,通常用于判断两个元素是否属于同一个集合。
并查集通常使用树结构来表示集合,其中每个节点表示一个元素,树的根节点表示集合的代表元素。通过路径压缩和按秩合并(没什么用)等优化方式,可以在近似常数时间内进行合并和查询操作。
二.并查集实现
1.存储方式
使用一维数组来实现,通过数组将元素连通在一起,就相当于将元素放在同一个集合中了。比如将A、B、C放在同一个集合,有
2.功能实现
const int N = 10010;
int father[N];
//并查集寻根的过程--递归
int find(int u)
{
//等于本身就是根节点,返回根节点
if (u == father[u])
return u;
//不是根节点,向下寻根
else
return find(father[u]);
}
// v -> u加入这条边
void join(int u, int v)
{
u = find(u);//寻根
v = find(v);//寻根
//同一集合就不用加入了
if (u == v)
return;
father[v] = u;
}
//判断是否同一个集合---是否同一个根
bool isSame(int u, int v)
{
u = find(u);//寻根
v = find(v);//寻根
return u == v;
}
//初始化---每个节点初始化指向自己
void init()
{
for (int i = 0; i < N; i++)
{
father[i] = i;
}
}
三.路径优化
对于一个集合,将其抽象成树形结构可能是:
对于find函数,其实是一个寻根过程,比如
对于插入一条边和查询是否同一个集合,都是通过查找根节点后进行操作的,那么find的效率就取决于路径的长度,既然都是找根,那么同一个集合的元素直接插到同一个节点不就行了,将树的结构改成:
这样每次find就不用递归那么多层了,这种思想就是路径压缩。
只需要改一下find函数就可以实现:
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
四.时间复杂度
路径压缩后的并查集时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。
五.并查集+路径压缩 模板
const int N = 10010;
int father[N];
//并查集寻根的过程--递归
int find(int u)
{
return u == father[u] ? u : father[u] = find(father[u]);
}
// v -> u加入这条边
void join(int u, int v)
{
u = find(u);//寻根
v = find(v);//寻根
//同一集合就不用加入了
if (u == v)
return;
father[v] = u;
}
//判断是否同一个集合---是否同一个根
bool isSame(int u, int v)
{
u = find(u);//寻根
v = find(v);//寻根
return u == v;
}
//初始化---每个节点初始化指向自己
void init()
{
for (int i = 0; i < N; i++)
{
father[i] = i;
}
}
五.题目
1971. 寻找图中是否存在路径 - 力扣(LeetCode)
class Solution {
//并查集
//模板实现
private:
int n = 200005;
vector<int> father = vector<int>(n, 0);
//初始化
void init()
{
for(int i = 0; i < n; i++)
{
father[i] = i;
}
}
//find找根,路径压缩实现
int find(int u)
{
return u == father[u] ? u : father[u] = find(father[u]);
}
//判断是否同根
bool isSame(int u, int v)
{
u = find(u);
v = find(v);
return u == v;
}
//加入同一集合 v -> u
void join(int u, int v)
{
u = find(u);
v = find(v);
if(u == v)
{
return;
}
father[v] = u;
}
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
init();
for(int i = 0; i < edges.size(); i++)
{
join(edges[i][0], edges[i][1]);
}
return isSame(source,destination);
}
};
684. 冗余连接 - 力扣(LeetCode)
class Solution {
//并查集
private:
int n = 1005;
vector<int> father = vector<int>(n, 0);
//初始化
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)
{
u = find(u);
v = find(v);
return u == v;
}
//加入同一集合
void join(int u, int v)
{
u = find(u);
v = find(v);
if(u == v)
{
return;
}
father[v] = u;
}
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
init();//记得初始化
for(int i = 0; i < n; i++)
{
if(isSame(edges[i][0], edges[i][1]))
return edges[i];
else
join(edges[i][0], edges[i][1]);
}
return {};
}
};
685. 冗余连接 II - 力扣(LeetCode)
class Solution {
private:
static const int N = 1010; // 如题:二维数组大小的在3到1000范围内
int father[N];
int n; // 边的数量
// 并查集初始化
void init() {
for (int i = 1; i <= n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return ;
father[v] = u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 在有向图里找到删除的那条边,使其变成树
vector<int> getRemoveEdge(const vector<vector<int>>& edges) {
init(); // 初始化并查集
for (int i = 0; i < n; i++) { // 遍历所有的边
if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边
return edges[i];
}
join(edges[i][0], edges[i][1]);
}
return {};
}
// 删一条边之后判断是不是树
bool isTreeAfterRemoveEdge(const vector<vector<int>>& edges, int deleteEdge) {
init(); // 初始化并查集
for (int i = 0; i < n; i++) {
if (i == deleteEdge) continue;
if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树
return false;
}
join(edges[i][0], edges[i][1]);
}
return true;
}
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int inDegree[N] = {0}; // 记录节点入度
n = edges.size(); // 边的数量
for (int i = 0; i < n; i++) {
inDegree[edges[i][1]]++; // 统计入度
}
vector<int> vec; // 记录入度为2的边(如果有的话就两条边)
// 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案
for (int i = n - 1; i >= 0; i--) {
if (inDegree[edges[i][1]] == 2) {
vec.push_back(i);
}
}
// 处理图中情况1 和 情况2
// 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树
if (vec.size() > 0) {
if (isTreeAfterRemoveEdge(edges, vec[0])) {
return edges[vec[0]];
} else {
return edges[vec[1]];
}
}
// 处理图中情况3
// 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了
return getRemoveEdge(edges);
}
};