并查集
- 并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。有一个联合-查找算法定义了两个用于此数据结构的操作:
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union:将两个子集合并成同一个集合。
- 如图,最简单的一种解释方法,上图中上面一行存的是元素,下面的一行存的是元素所属的集合。那么我们可以清晰的看到:其中元素0、2、4、6、8是一个组别,元素1、3、5、7、9是属于一个组别。
第一种实现方法
- find 函数直接取的是元素的id,时间复杂度是O(1)
- UnionElements函数:比如要想元素1和2同属于一个组,那么这种方法就是将元素1同属的那个组的所有元素id都改成0,这样就实现了两个元素处在同一个组的效果。
#include<iostream>
#include<cassert>
using namespace std;
class UnionFind {
private:
int* id;
int count;
public:
UnionFind(int n)
{
count = n;
id = new int[n];
for (int i = 0; i < n; i++)
id[i] = i;
}
~UnionFind()
{
delete[] id;
}
int find(int p)
{
assert(p >= 0 && p < count);
return id[p];
}
bool isConnected(int p, int q)
{
return find(p) == find(q);
}
void unionElements(int p, int q)
{
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
for (int i = 0; i < count; i++)
{
if (id[i] == pID)
id[i] = qID;
}
}
};
第二种实现方式
- 将每一个元素看成一个节点,这个节点有他的父节点。先看个图。
- 对于上图。比如说,6是一个节点,他有一个父节点是5,说明他和5是同一个组别,那么5有一个父节点是2,那么说明5和2是同一个组别。类似的分析,我们说上面的都是一个组别。
- 对于上图,我们可以看到有两个组别,主要看下面所表示的。元素0、1、2、7它的父节点都是1。还有元素5的父节点是0,但是0的父节点又是1,因此元素0、1、2、5、6、7同属于一个组别。
#include<iostream>
#include<cassert>
using namespace std;
class UnionFind {
private:
int* parent;
int count;
public:
UnionFind(int n)
{
count = n;
parent = new int[n];
for (int i = 0; i < n; i++)
parent[i] = i;
}
~UnionFind()
{
delete[] parent;
}
int find(int p)
{
assert(p >= 0 && p < count);
while (p != parent[p])
p = parent[p];
return p;
}
bool isConnected(int p, int q)
{
return find(p) == find(q);
}
void unionElements(int p, int q)
{
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
parent[pRoot] = qRoot;
}
};
第三种实现方式
- 来张图看看,第二种实现思路中,我们说,将一个元素插到一个组别的时候,并没有考虑,那个组的元素多,插入那个组更高效。
- 比如将4和9联合一下,那么如果将4插入到9所属的组中,这样做明显的使查找速率变慢。那么我们维护一个组别的sz记录这个组别中的元素个数。我们永远将元素少的组别连接到元素多的组别。
#include<iostream>
#include<cassert>
using namespace std;
class UnionFind {
private:
int* parent;
int* sz; //sz[i] 表示以i为根的集合中元素个数
int count;
public:
UnionFind(int n)
{
count = n;
parent = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i;
sz[i] = 1;
}
}
~UnionFind()
{
delete[] parent;
delete[] sz;
}
int find(int p)
{
assert(p >= 0 && p < count);
while (p != parent[p])
p = parent[p];
return p;
}
bool isConnected(int p, int q)
{
return find(p) == find(q);
}
void unionElements(int p, int q)
{
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
if (sz[pRoot] < sz[qRoot])
{
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else
{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
};
第四种实现方式
- 我们看上图,对于第三种实现方式是用组别元素的个数来确定要将那个组指向那个组。对比上图,将跟节点是7的结点指向根节点是8的方法合并后层数是3.但是将跟节点是8的结点指向跟节点是7的方法合并后层数是4。因此我们说用组别中元素的个数来衡量也是有缺陷的。
- 这里使用的rank数组来表示树的层数。我们根据数的层数来做优化。将层数少的合并到层数多的树中。
#include<iostream>
#include<cassert>
using namespace std;
class UnionFind {
private:
int* parent;
int* rank; //rank[i] 表示以i为根的集合所表示的树的层数
int count;
public:
UnionFind(int n)
{
count = n;
parent = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i;
rank[i] = 1;
}
}
~UnionFind()
{
delete[] parent;
delete[] rank;
}
int find(int p)
{
assert(p >= 0 && p < count);
while (p != parent[p])
p = parent[p];
return p;
}
bool isConnected(int p, int q)
{
return find(p) == find(q);
}
void unionElements(int p, int q)
{
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
if (rank[pRoot] < rank[qRoot])
{
parent[pRoot] = qRoot;
}
else if(rank[qRoot] < rank[pRoot])
{
parent[qRoot] = pRoot;
}
else //rank[pRoot] == rank[qRoot]
{
parent[pRoot] = qRoot;
rank[qRoot] += 1;
}
}
};
第五种方法
- 从上面的例子中,我们看到一个父节点可以有无数的子节点,所以我们在找的时候就很浪费时间。但是当我们每次联合的时候,可以将最低层的节点一点一点的往上移。也就是说,我们将一个子节点可以连接到他父亲的父亲节点,如果可以,那我们可以连接他父亲的父亲的父亲,一直到连接到这个组别的跟节点。我们期望最后的组别就只有两层。
- 代码实现非常简单了,只需要将find函数加上一句话就好了。
- 我们也将此算法称为路径压缩。
#include<iostream>
#include<cassert>
using namespace std;
class UnionFind {
private:
int* parent;
int* rank; //rank[i] 表示以i为根的集合所表示的树的层数
int count;
public:
UnionFind(int n)
{
count = n;
parent = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i;
rank[i] = 1;
}
}
~UnionFind()
{
delete[] parent;
delete[] rank;
}
int find(int p)
{
assert(p >= 0 && p < count);
while (p != parent[p])
{
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
bool isConnected(int p, int q)
{
return find(p) == find(q);
}
void unionElements(int p, int q)
{
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
if (rank[pRoot] < rank[qRoot])
{
parent[pRoot] = qRoot;
}
else if(rank[qRoot] < rank[pRoot])
{
parent[qRoot] = pRoot;
}
else //rank[pRoot] == rank[qRoot]
{
parent[pRoot] = qRoot;
rank[qRoot] += 1;
}
}
};
第六种优化方法
- 我们采取递归的算法,将上面的路径优化一直优化到只有两层的树。
- 只需要将上面的find函数改变就好了。我下面只放我的find函数。
int find(int p)
{
assert(p >= 0 && p < count);
if (p != parent[p])
parent[p] = find(parent[p]);
return parent[p];
}