并查集
并查集是一种树型的数据结构,用于处理(不相交)集合的合并、查询及由此衍生的问题。其功能主要有三个:
- 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
- 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
- 判断两个节点是否在同一个集合,函数:same(int u, int v),就是判断两个节点是不是同一个根节点
实现
并查集的实现分为四个部分:
初始化
初始化时把每个节点的祖先节点设为自身,可看作创建了一个大的森林。
int n = 1005; // 假设节点数在1005以内
int father[1005]; // father并不代表父节点而是代表根节点
void init() {
for (int i = 0; i < 1005; i++)
father[i] = i;
}
查找祖先节点
查找节点 u 对应的祖先节点,同时保存它的祖先节点。
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
使用递归查找,第一次要一直找到本身值与祖先节点值相等的节点(即根节点),因为沿途的祖先节点的值都保存下来了,所以以后对沿途的节点查找祖先节点只需要一次递归。
是否在同一个集合
通过祖先节点(根节点)是否相同判断两个节点是否在同一个集合中。
bool same(int u, int v) {
return find(u) == find(v);
}
合并
把两个节点合并到一个集合上。
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
先查找两个节点是否已在同一个集合上,若不是,则合并。注意,这里不能用same来判断是否在同一个集合上,因为我们除了要判断是否在同一个集合上,还要找到两个节点的根节点以合并,即合并不只是合并两个节点,而是合并这两个节点所在的集合。
例题
684.冗余连接
这道题就可以用并查集来解决,我们变脸edges数组,对其中每个边的两个节点将其加入同一个集合中,若发现这两个节点已经加入,则说明再加入此边就会形成环,则这一条边必不能存在,必须删除。
因为是要返回最右边需要删除的边,所以我们从左至右遍历,为什么返回最右边还要从左至右呢。因为我们是从左至右建立集合直到找到一个必须要删除的边,那后面的边可不可能是答案呢,不可能,因为我们找到的边是一定会导致环的边,即这条边一定要删除,删除后面的边而不删除这条边得到的树不满足题意,所以返回从左至右找到的第一条不满足条件的边。
代码如下:
class Solution {
private:
int n = 1005;
int father[1005];
void init() {
for (int i = 0; i < 1005; i++)
father[i] = i;
}
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
bool same(int u, int v) {
return find(u) == find(v);
}
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
init();
for (int i = 0; i < edges.size(); i++) {
if (same(edges[i][0], edges[i][1])) return edges[i];
else join(edges[i][0], edges[i][1]);
}
return {};
}
};
685.冗余连接II
这题和冗余连接差别还是挺大的,因为冗余连接里面只有形成环的情况,而这道题还有更加复杂的情况。
往题中所说的树加一条边,要么加到根节点上,则必形成环,要么加到其他节点上,可能形成环也可能不形成环(注意这里所说的环是指有向图的环!)。我们必须先讨论加到其他节点的情况。加到其他节点,必然造成某一节点入度为2,所以我们统计入度为2的节点的两条边,注意要倒序统计,因为题目要求返回最后出现在二维数组的答案。统计完成后我们就尝试删除。删除也有两种情况,一种是删除两条边任意一条即可,这种情况我们就删靠后的;另一种情况是只有其中一条符合条件,因为删除另外一条不能消除环,判断是否有环还是用上面的方法。如果是加到根节点的情况,就会形成环,则又和冗余连接那道题相同了。代码如下:
class Solution {
private:
static const int n = 1005;
int father[n];
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
bool same(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
bool IstreeAfterRemove(vector<vector<int>> &edges, int delete_edge) {
init();
for (int i = 0; i < edges.size(); ++i) {
if (i == delete_edge) continue;
if (same(edges[i][0], edges[i][1]))
return false;
join(edges[i][0], edges[i][1]);
}
return true;
}
vector<int> RemoveEdge(vector<vector<int>> &edges) {
init();
for (int i = 0; i < edges.size(); ++i) {
if (same(edges[i][0], edges[i][1]))
return edges[i];
join(edges[i][0], edges[i][1]);
}
return {-1};
}
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int degree[n] = {0};
for (int i = 0; i <edges.size(); ++i) {
degree[edges[i][1]]++;
}
vector<int> two_degree;
for (int i = edges.size() - 1; i >= 0; --i) {
if (degree[edges[i][1]] == 2)
two_degree.push_back(i);
}
if (two_degree.size() > 0) {
if (IstreeAfterRemove(edges, two_degree[0]))
return edges[two_degree[0]];
else
return edges[two_degree[1]];
}
return RemoveEdge(edges);
}
};