关于并查集,看了好多博客,我理解的并查集是一种数据结构,主要用于解决动态连通性一类问题。用于处理一些不相加集合的合并和查询问题。在使用中常常以森林来表示。 并查集也是用来维护集合的,和前面学习的set不同之处在于,并查集能很方便地同时维护很多集合。如果用set来维护会非常的麻烦。并查集的核心思想是记录每个结点的父亲结点是哪个结点。
参考博客:https://blog.csdn.net/dm_vincent/article/details/7655764
假设我们输入了一组整数对,即上图中的(4, 3) (3, 8)等等,每对整数代表这两个points/sites是连通的。那么随着数据的不断输入,整个图的连通性也会发生变化,从上图中可以很清晰的发现这一点。同时,对于已经处于连通状态的points/sites,直接忽略,比如上图中的(8, 9)。
就动态连通性这个场景而言,我们需要解决的问题是:
还有的博客给出了地图的例子,判断两个地点是否连通,也很形象。
现在给出一组数据,其中每个元素都是一对“点”,代表这对点之间是联通的,我们需要设计一个算法,让计算机依次读取这些数据,最后判断出其中任意两点是否连通。注意,并查集所涉及的动态连通性只是考虑“是否连通”这一二值判别问题,而不涉及连通的路径到底是什么。后者不在本文的考虑范围之内。
两种优化方法:路径压缩和按秩合并
两种常见的优化策略:一是路径压缩策略,即当调用一次find(x)时,顺带将x指向根节点;二是按秩合并,即将有较少节点的树的根指向具有较多节点的树的根
1) 初始化:初始的时候每个结点各自为一个集合,father[i]表示结点 i 的父亲结点,如果 father[i]=i,我们认为这个结点是当前集合根结点。
void init() {
for (int i = 1; i <= n; ++i) {
father[i] = i;
}
}
2) 查找:查找结点所在集合的根结点,结点 x 的根结点必然也是其父亲结点的根结点。
int get(int x) {
if (father[x] == x) { // x 结点就是根结点
return x;
}
return get(father[x]); // 返回父结点的根结点
}
3) 合并:将两个元素所在的集合合并在一起,通常来说,合并之前先判断两个元素是否属于同一集合。
void merge(int x, int y) {
x = get(x);
y = get(y);
if (x != y) { // 不在同一个集合
father[y] = x;
}
}
4)统计个数
int getSubsetNum()
{
int res = 0;
for(int i = 1; i <= num; i++)
if(father[i] == i)
res++;
return res;
}
5)路径压缩:我们在一次查询的时候,可以把查询路径上的所有结点的father[i]都赋值成为根结点。路径压缩在实际应用中效率很高,其一次查询复杂度平摊下来可以认为是一个常数。并且在实际应用中,我们基本都用带路径压缩的并查集,代码为:
int get(int x) {
if (father[x] == x) { // x 结点就是根结点
return x;
}
return father[x] = get(father[x]); // 返回父结点的根结点,并另当前结点父结点直接为根结点
}
1、朋友圈
题目:
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
解析:
典型的并查集问题,实质上是求无向图的连通分支数。只需要将原始的集合分成几个子集合,每个子集合代表一个朋友圈即可。
int findCircleNum(vector>& M)
{
if (M.size() == 0) return 0;
num = M.size();
init();
for (int i = 0; i < M.size(); i++)
for (int j = i + 1; j < M.size(); j++)
{
if (M[i][j])
merge(i + 1, j + 1);
}
return getSubsetNum();
}
另外,也可使用dfs进行求解(参考https://www.cnblogs.com/grandyang/p/6686983.html)的代码:
int findCircleNum(vector>& M) {
int n = M.size(), res = 0;
vector visited(n, false);
for (int i = 0; i < n; ++i) {
if (visited[i]) continue;
helper(M, i, visited);
++res;
}
return res;
}
void helper(vector>& M, int k, vector& visited) {
visited[k] = true;
for (int i = 0; i < M.size(); ++i) {
if (!M[k][i] || visited[i]) continue;
helper(M, i, visited);
}
}
2、岛屿的个数
题目:
给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
解析:
在这里用并查集实现。先令并查集内个节点的值为0,每当遇到一块陆地时,如果它未被放入并查集,则放入并查集;然后遍历四周陆地,如果未放入并查集,也将其放入;然后union两块陆地。最后并查集中子集个数即为答案
class Solution {
public:
int findFather(vector &father,int a){
int f=a;
while(father[f]!=f){
f=father[f];
}
while(father[a]!=a){
int z=a;
a=father[z];
father[a]=f;
}
return f;
}
void unionFather(vector &father, int a,int b){
int fa=findFather(father,a);
int fb=findFather(father,b);
if(fa!=fb){
father[fa]=fb;
}
}
int numIslands(vector>& grid) {
if(grid.size()==0||grid[0].size()==0) return 0;
int n=grid.size(),m=grid[0].size(),k=n*m;
vector father(k,-1);
for(int i=0;i
和第一题一样,这个也可以用dfs解决:
class Solution {
public:
int numIslands(vector>& grid) {
int count = 0;
for(int i = 0; i < grid.size(); i ++){
for(int j = 0; j < grid[0].size(); j ++){
if(grid[i][j] >= '1'){
count ++;
dfs(grid, i, j);
}
}
}
return count;
}
void dfs(vector>& grid, int i, int j){
if(i >= grid.size() || i < 0 || j < 0 || j >= grid[0].size() || grid[i][j] != '1')
return;
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
};