并查集:基本操作、路径压缩

目录

概念

实现

基本操作

初始化

查找

合并

路径压缩

例题:305.岛屿数量Ⅱ


概念

并查集是一种维护集合的数据结构,它的名字中“并"“查”“集”分别取自Union(合并)、Find(查找)、Set(集合)这三个单词。

  • 合并:合并两个集合
  • 查找:判断两个元素是否在一个集合

实现

int father[N];

其中father[i]表示元素i的父亲节点,而父亲节点本身也是这个集合内的元素。

如果father[i]=i,那么i就是该集合的根节点。

举例: 

father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 2;
father[5] = 5;
father[6] = 5;

基本操作

初始化

查找

同一个集合中只存在一个根节点,因此查找操作就是对给定结点寻找根节点的过程。

即反复寻找父亲节点。

递推:

int findFather(int x) {
	while (x != father[x]) {
		x = father[x];
	}
	return x;
}

递归:

int findFather(int x) {
	if (x == father[x])return x;
	else return findFather(father[x]);
}

合并

把两个集合合并成一个元素,具体实现上一般是先判断两个元素是否属于同一个集合,只有两个元素属于不同集合才合并。

合并的过程一般是把其中一个集合的根节点的父亲指向另一个集合的根节点:

int Union(int a, int b) {
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA != faB) {
		father[faA] = faB;
	}
}

需要注意的是,不能直接将其中一个元素的父亲设为另一个元素,即直接father[a]=b;:

路径压缩

上面提到的查找是没有经过优化的,在极端情况下效率很低。

假设元素很多,并且形成了一条链,那么这个查找效率就会很低:

如果要进行10^5次查询,且每次查询都要查询最后面结点的根节点,那么每次就要花费10^5的计算量进行查找,无法接受。

father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 3;

findFather的目的就是查找根节点,因此,如果只是为了查找根节点,那么完全可以想办法把操作等价的变成: 

father[1] = 1;
father[2] = 1;
father[3] = 1;
father[4] = 1;

即:

相当于把当前查询节点路径上的所有结点的父亲都指向根节点。这样查找就不用一直回溯去找父亲,复杂度变为O(1):

  • 按原先的方法获得x的根节点r
  • 重新从x开始走一遍过程,把路径上所有经过的结点的父亲全部改为根节点

递推:

int findFather(int x) {
	int a = x;
	while (x != father[x]) {
		x = father[x];
	}
	while (a != father[a]) {
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}

递归: 

int findFather(int x) {
	if (x == father[x])return x;
	else {
		int F = findFather(father[x]);
		father[x] = F;
		return F;
	}
}

例题:305.岛屿数量Ⅱ

305. 岛屿数量 II - 力扣(LeetCode)

给你一个大小为 m x n 的二进制网格 grid 。网格表示一个地图,其中,0 表示水,1 表示陆地。最初,grid 中的所有单元格都是水单元格(即,所有单元格都是 0)。

可以通过执行 addLand 操作,将某个位置的水转换成陆地。给你一个数组 positions ,其中 positions[i] = [ri, ci] 是要执行第 i 次操作的位置 (ri, ci) 。

返回一个整数数组 answer ,其中 answer[i] 是将单元格 (ri, ci) 转换为陆地后,地图中岛屿的数量。

岛屿 的定义是被「水」包围的「陆地」,通过水平方向或者垂直方向上相邻的陆地连接而成。你可以假设地图网格的四边均被无边无际的「水」所包围。

 示例 1:

输入:m = 3, n = 3, positions = [[0,0],[0,1],[1,2],[2,1]]
输出:[1,1,2,3]
解释:
起初,二维网格 grid 被全部注入「水」。(0 代表「水」,1 代表「陆地」)
- 操作 #1:addLand(0, 0) 将 grid[0][0] 的水变为陆地。此时存在 1 个岛屿。
- 操作 #2:addLand(0, 1) 将 grid[0][1] 的水变为陆地。此时存在 1 个岛屿。
- 操作 #3:addLand(1, 2) 将 grid[1][2] 的水变为陆地。此时存在 2 个岛屿。
- 操作 #4:addLand(2, 1) 将 grid[2][1] 的水变为陆地。此时存在 3 个岛屿。


示例 2:

输入:m = 1, n = 1, positions = [[0,0]]
输出:[1]

刚开始的思路是:先把海填完,然后在套用200. 岛屿数量 - 力扣(LeetCode)的套路,即DFS,转化为岛屿数量的问题。但是这道题是需要”填一次海,数一次数量“,故这种思路不太可取。

我们思考并查集,每一次填海,看周围是否有已经填好的陆地(上下左右),如有则把他们并入集合,并返回此时的岛屿数量(集合数量),故此题适合用并查集来做。

首先定义并查集相关操作:

class UnionFind {
public:
	int cnt;
	vector<int>father;

	UnionFind(int n) {
		cnt = 0;//初始时集合(岛屿)数量为0
		father.resize(n);
		for (int i = 0; i < n; i++) {
			father[i] = i;
		}
	}

	int findFather(int x) {
		while (x != father[x]) {
			x = father[x];
		}
		return x;
	}

	void Union(int x, int y) {
		int root_x = findFather(x);
		int root_y = findFather(y);
		if (root_x == root_y)return;
		if (root_x != root_y) {
			father[root_x] = root_y;
		}
		cnt--;//集合数量-1
	}

	bool isConnent(int x, int y) {
		return findFather(x) == findFather(y);//两个岛屿是否连通
	}

	int count() {
		return cnt;
	}

	void add() {
		cnt++;//填海的时候,集合(岛屿)数+1
	}
};

然后看核心代码块:

class Solution {
public:
	int n, m;
	bool isValid(int x, int y) {
		return x >= 0 && x < m&& y >= 0 && y < n;
	}
	vector<int> numIslands2(int _m, int _n, vector<vector<int>>& positions) {
		
	}
};

 首先把判断边界函数抽象出来。

class Solution {
public:
	int n, m;
	bool isValid(int x, int y) {
		return x >= 0 && x < m&& y >= 0 && y < n;
	}
	vector<int> numIslands2(int _m, int _n, vector<vector<int>>& positions) {
		m = _m;
		n = _n;
		//定义上下左右方向
		vector<int>dirX{ -1,1,0,0 };
		vector<int>dirY{ 0,0,-1,1 };
		//初始化并查集
		UnionFind u(m * n);
		//初始化标记数组和最后返回的答案
		vector<bool>vis(m * n, false);
		vector<int>ans;

		for (auto& pos : positions) {
			int x = pos[0];
			int y = pos[1];
			int index = x * n + y;//填海坐标,二维下标的一维表示
			if (vis[index] == false) {
				vis[index] = true;
				u.add();
				for (int i = 0; i < 4; i++) {
					int xx = x + dirX[i];
					int yy = y + dirY[i];
					int newIndex = xx * n + yy;//周围区域坐标
					//如果下标有效 && 此区域已被访问 && 两区域不连通
					if (isValid(xx, yy) && vis[newIndex] == true && u.isConnent(index, newIndex) == false) {
						u.Union(index, newIndex);
					}
				}
			}
			ans.push_back(u.count());
		}
		return ans;
	}
};

思路可以看注释。

对于每一次填海,我们都将其坐标位置标记为true,此时集合数+1。

然后,对于每一次填海,我们查找上下左右四个方向是否已经有填好的海,如有,则合并,集合数-1.

每一次填海,都记录一次答案。

最后返回每次记录的答案ans。 

参考:《算法笔记》-胡凡

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值