本文用于个人算法竞赛学习,仅供参考
目录
一.什么是并查集
二.并查集实现
三.路径优化
四.时间复杂度
五.并查集+路径压缩 模板
五.题目
一.什么是并查集
并查集(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;//不是根节点,向下寻根elsereturn 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 -> uvoid 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];elsejoin(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);}
};