并查集(Union-Find)是一种用于处理集合关系的算法,在连通图的相关问题中非常实用。基本的操作包括集合的并(Union)和查找元素所属集合(Find)。
算法思路如下:每一个集合,都有一个代表元。在同一个集合中的所有元素,它们的代表元必然相同。要区分一个元素属于哪一个集合,找到它的代表元即可。
其存储结构通常为一个一维数组(N代表元素总数):
int father[N + 1] = { 0 };
初始化如下:
for (int i = 1; i <= N; i++) { father[i] = i; }
那么根据定义,用于查找某个元素所在集合的代表元的函数可以这样实现:
int getFather(int x) {
if (father[x] == x) {
return x;
} else {
return getFather(father[x]);
}
}
但是,当元素数量很大,多次的递归显然效率较低。那么,可以引入“压缩路径”的方法:
int getFather(int x) {
if (father[x] == x) {
return x;
}
else {
int xFather = getFather(father[x]);
father[x] = xFather;
return xFather;
}
}
这样,在每一次递归之后,都会更新father[x]的值,实现了压缩路径的目的,避免了多次重复的递归。这种方式实现的并查集,时间复杂度为O(n)
。
实现了查找操作以后,便可以在此基础上进行集合的并操作,基本实现如下:
void union(int x, int y) {
int xFather = getFather(x);
int yFather = getFather(y);
father[yFather] = xFather;
}
即把后一个元素y并入了x所属的集合中,若x和y分别为两个集合的代表元,那么就是把x和y所在的两个集合进行了并的操作。
基本掌握了并查集的算法后,就可以很好的解决LeetCode的684 Redundant Connection和685 Redundant Connection II了。
684 Redundant Connection
In this problem, a tree is an undirected graph that is connected and has no cycles.
The given input is a graph that started as a tree with N nodes (with distinct values 1, 2, …, N), with one additional edge added. The added edge has two different vertices chosen from 1 to N, and was not an edge that already existed.The resulting graph is given as a 2D-array of edges. Each element of edges is a pair [u, v] with u < v, that represents an undirected edge connecting nodes u and v.
Return an edge that can be removed so that the resulting graph is a tree of N nodes. If there are multiple answers, return the answer that occurs last in the given 2D-array. The answer edge [u, v] should be in the same format, with u < v.
Example 1:
Input: [[1,2], [1,3], [2,3]]
Output: [2,3]
Explanation: The given undirected graph will be like this:
1
/ \
2 - 3
Example 2:
Input: [[1,2], [2,3], [3,4], [1,4], [1,5]]
Output: [1,4]
Explanation: The given undirected graph will be like this:
5 - 1 - 2
| |
4 - 3
Note:
The size of the input 2D-array will be between 3 and 1000.
Every integer represented in the 2D-array will be between 1 and N, where N is the size of the input array.
题目大意是:输入给出了一个有N个结点的,连通的,无环的无向图。而图中有一条多余的边,需要找出这一条边。
利用并查集的算法,可以很轻松的解决这道题。遍历输入,在遍历的同时构造集合。当找到一条边,它的两个端点已经是属于同一个集合中,那么这条边显然是多余的。否则,继续进行并的操作。而题目要求在有多个解时返回最后一个,那么只需记录下这些多余的边,返回最后一次更新的边即可。代码实现如下:
class Solution {
public:
int father[1001] = { 0 };
int getFather(int node) {
if (father[node] == node) {
return node;
}
else {
int nodeFather = getFather(father[node]);
father[node] = nodeFather;
return nodeFather;
}
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
for (int i = 1; i <= 1000; i++) {
father[i] = i;
}
vector<int> result;
for (auto it = edges.begin(); it != edges.end(); it++) {
int aFather = getFather(it->at(0));
int bFather = getFather(it->at(1));
if (aFather != bFather) {
father[bFather] = aFather;
}
else {
result.clear();
result.push_back(it->at(0));
result.push_back(it->at(1));
}
}
return result;
}
};
而对于Redundant Connection II,则需要在并查集的基础上稍加扩展。
In this problem, a rooted tree is a directed graph such that, there is exactly one node (the root) for which all other nodes are descendants of this node, plus every node has exactly one parent, except for the root node which has no parents.
The given input is a directed graph that started as a rooted tree with N nodes (with distinct values 1, 2, …, N), with one additional directed edge added. The added edge has two different vertices chosen from 1 to N, and was not an edge that already existed.
The resulting graph is given as a 2D-array of edges. Each element of edges is a pair [u, v] that represents a directed edge connecting nodes u and v, where u is a parent of child v.
Return an edge that can be removed so that the resulting graph is a rooted tree of N nodes. If there are multiple answers, return the answer that occurs last in the given 2D-array.
Example 1:
Input: [[1,2], [1,3], [2,3]]
Output: [2,3]
Explanation: The given directed graph will be like this:
1
/ \
v v
2–>3
Example 2:
Input: [[1,2], [2,3], [3,4], [4,1], [1,5]]
Output: [4,1]
Explanation: The given directed graph will be like this:
5 <- 1 -> 2
^ |
| v
4 <- 3
Note:
The size of the input 2D-array will be between 3 and 1000.
Every integer represented in the 2D-array will be between 1 and N, where N is the size of the input array.
这次输入的是一个有向图,且规定:有这么一个结点(根结点),所有的其它结点都是它的子结点。且除了根结点(入度为0)外的所有结点,都有且只有一个父节点(入度为1)。在输入中,会有这么一条边:当这条边被移除后,所得到的图才是符合上述条件的图。
根据题意,我们可以得出一个结论:当所要找到的这条边去除后,所得到的图一定是符合条件的。也就是说,这一条边,破坏了原图的合法性。
那么,怎么样的图才是不符合条件的呢:
确定了这两个条件之后,我们可以得到进一步的推论:如果图中有环,则去掉的是环中的一条边。若存在入度为2的结点,那么以该结点为子结点的两条边中,必然有一条就是将要被去除的边。根据上述的推论,可以得到以下算法:
在图中寻找一个入度为2的点
若 不存在
则 图中有环
找到最后一条使图成环的边 并返回
若 存在
获取与该结点相关的两条边A,B
将B移除 得到新的图
若 新的图符合条件
返回B
若 不符合条件
返回A
根据这个算法,得到代码实现如下:
class Solution {
public:
int father[1001] = { 0 };
int getFather(int node) {
if (father[node] == node) {
return node;
}
else {
int nodeFather = getFather(father[node]);
father[node] = nodeFather;
return nodeFather;
}
}
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
for (int i = 1; i <= 1000; i++) {
father[i] = i;
}
vector<int> resultA, resultB;
int parent[1001] = { 0 };
// 找到入度为2的结点,并获取候选边A,B
for (auto it = edges.begin(); it != edges.end(); it++) {
if (parent[it->at(1)] == 0) {
parent[it->at(1)] = it->at(0);
}
else {
resultA.push_back(parent[it->at(1)]);
resultA.push_back(it->at(1));
resultB.push_back(it->at(0));
resultB.push_back(it->at(1));
it->clear(); // 从图中移除B
}
}
for (auto it = edges.begin(); it != edges.end(); it++) {
if (it->empty()) {
continue;
}
int aFather = getFather(it->at(0));
int bFather = getFather(it->at(1));
if (aFather == it->at(1)) { // 判断有环
if (resultA.empty()) { // 不存在入度为2的结点
return *it;
}
else {
return resultA;
}
}
father[bFather] = aFather;
}
return resultB;
}
};
要注意各个条件的判断顺序。例如:不能直接去掉使图成环的最后一条边。因为这样还可能存在入度为2的结点。在确定图中有环后,应该优先判断是否存在入度为2的结点,再考虑环的问题。