算法拾遗二十之并查集相关题解

算法拾遗二十之并查集应用相关题解

省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
在这里插入图片描述
在这里插入图片描述

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/number-of-provinces
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

// 本题为leetcode原题
// 测试链接:https://leetcode.cn/problems/number-of-provinces
// 可以直接通过
public class Code01_FriendCircles {

	public static int findCircleNum(int[][] M) {
		int N = M.length;
		// {0} {1} {2} {N-1}
		UnionFind unionFind = new UnionFind(N);
		//遍历右上角则好了因为[i][j]认识那么[j][i]肯定也认识
		for (int i = 0; i < N; i++) {
			for (int j = i + 1; j < N; j++) {
				if (M[i][j] == 1) { // i和j互相认识
					unionFind.union(i, j);
				}
			}
		}
		return unionFind.sets();
	}

	public static class UnionFind {
		// parent[i] = k : i的父亲是k
		private int[] parent;
		// size[i] = k : 如果i是代表节点,size[i]才有意义,否则无意义
		// i所在的集合大小是多少
		private int[] size;
		// 辅助结构
		private int[] help;
		// 一共有多少个集合
		private int sets;

		public UnionFind(int N) {
			parent = new int[N];
			size = new int[N];
			help = new int[N];
			sets = N;
			for (int i = 0; i < N; i++) {
				parent[i] = i;
				size[i] = 1;
			}
		}

		// 从i开始一直往上,往上到不能再往上,代表节点,返回
		// 这个过程要做路径压缩
		private int find(int i) {
			int hi = 0;
			while (i != parent[i]) {
				help[hi++] = i;
				i = parent[i];
			}
			for (hi--; hi >= 0; hi--) {
				parent[help[hi]] = i;
			}
			return i;
		}

		public void union(int i, int j) {
			int f1 = find(i);
			int f2 = find(j);
			if (f1 != f2) {
				if (size[f1] >= size[f2]) {
					size[f1] += size[f2];
					parent[f2] = f1;
				} else {
					size[f2] += size[f1];
					parent[f1] = f2;
				}
				sets--;
			}
		}

		public int sets() {
			return sets;
		}
	}

}

岛屿数量

https://leetcode.cn/problems/number-of-islands/

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/number-of-islands
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

深度优先遍历方式:

public static int numIslands3(char[][] board) {
		int islands = 0;
		for (int i = 0; i < board.length; i++) {
			for (int j = 0; j < board[0].length; j++) {
				if (board[i][j] == '1') {
					islands++;
					//感染算法,通过递归去找上下左右所有相关的1
					infect(board, i, j);
				}
			}
		}
		return islands;
	}

	// 从(i,j)这个位置出发,把所有连成一片的'1'字符,变成0
	public static void infect(char[][] board, int i, int j) {
		if (i < 0 || i == board.length || j < 0 || j == board[0].length || board[i][j] != '1') {
			return;
		}
		board[i][j] = 0;
		//感染上面
		infect(board, i - 1, j);
		//感染下面
		infect(board, i + 1, j);
		//感染左边
		infect(board, i, j - 1);
		//感染右边
		infect(board, i, j + 1);
	}

时间复杂度估算为,如果这个矩阵为MN的话,那么这个时间复杂度为MN,
在主流程里面数组的每个位置最多遍历一遍,在infect的感染流程里面,每个位置只可能被它的上邻居,下邻居,左邻居,右邻居各碰一次。【一个位置最多被碰5次,所以总复杂度为O(M*N)】

并查集方式:
先对所有的1做初始化,如果一个1的左边和上边有1则合并,因为上下左右是相对的,只查一个点的左边跟上边,如果它右边
有1的话那么它会查它的左边则不会错过那个1,如果一个1的下边有1的话,那么下边那个1可以通过查上边的1来找到这个1,从而不会错过。所以只查两个方向就好了。

	public static int numIslands1(char[][] board) {
		int row = board.length;
		int col = board[0].length;
		Dot[][] dots = new Dot[row][col];
		List<Dot> dotList = new ArrayList<>();
		for (int i = 0; i < row; i++) {
			for (int j = 0; j < col; j++) {
				if (board[i][j] == '1') {
					//用dot每次new对象不同的内存地址来标识这个相同数值但不同位置的1
					dots[i][j] = new Dot();
					dotList.add(dots[i][j]);
				}
			}
		}
		UnionFind1<Dot> uf = new UnionFind1<>(dotList);
		//为了不判断边界条件,并且降低常数复杂度,此处先遍历第零行【只有左没有上面的位置】,再遍历第零列【只有上面的位置】
		//剩下的则是既有左又有上的,则都能判断
		//由于(0,0)位置既没有左也没有上无意义则跳过
		for (int j = 1; j < col; j++) {
			// (0,j)  (0,0)跳过了  (0,1) (0,2) (0,3)
			//如果左边是1,并且我当前是1,则通过dot去合并
			if (board[0][j - 1] == '1' && board[0][j] == '1') {
				uf.union(dots[0][j - 1], dots[0][j]);
			}
		}
		for (int i = 1; i < row; i++) {
			//如果上边是1,并且我当前是1,则通过dot去合并
			if (board[i - 1][0] == '1' && board[i][0] == '1') {
				uf.union(dots[i - 1][0], dots[i][0]);
			}
		}
		for (int i = 1; i < row; i++) {
			for (int j = 1; j < col; j++) {
				if (board[i][j] == '1') {
					//如果左边是1合并
					if (board[i][j - 1] == '1') {
						uf.union(dots[i][j - 1], dots[i][j]);
					}
					//如果上边是1合并
					if (board[i - 1][j] == '1') {
						uf.union(dots[i - 1][j], dots[i][j]);
					}
				}
			}
		}
		return uf.sets();
	}

	public static class Dot {

	}

	public static class Node<V> {

		V value;

		public Node(V v) {
			value = v;
		}

	}

	public static class UnionFind1<V> {
		public HashMap<V, Node<V>> nodes;
		public HashMap<Node<V>, Node<V>> parents;
		public HashMap<Node<V>, Integer> sizeMap;

		public UnionFind1(List<V> values) {
			nodes = new HashMap<>();
			parents = new HashMap<>();
			sizeMap = new HashMap<>();
			for (V cur : values) {
				Node<V> node = new Node<>(cur);
				nodes.put(cur, node);
				parents.put(node, node);
				sizeMap.put(node, 1);
			}
		}

		public Node<V> findFather(Node<V> cur) {
			Stack<Node<V>> path = new Stack<>();
			while (cur != parents.get(cur)) {
				path.push(cur);
				cur = parents.get(cur);
			}
			while (!path.isEmpty()) {
				parents.put(path.pop(), cur);
			}
			return cur;
		}

		public void union(V a, V b) {
			Node<V> aHead = findFather(nodes.get(a));
			Node<V> bHead = findFather(nodes.get(b));
			if (aHead != bHead) {
				int aSetSize = sizeMap.get(aHead);
				int bSetSize = sizeMap.get(bHead);
				Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
				Node<V> small = big == aHead ? bHead : aHead;
				parents.put(small, big);
				sizeMap.put(big, aSetSize + bSetSize);
				sizeMap.remove(small);
			}
		}

		public int sets() {
			return sizeMap.size();
		}

	}

当然如上并查集通过表实现常数时间并不好,所以优化为通过数组去表示这个并查集:
这里则是用下标对应的index去替换上面的dot对象。

public static int numIslands2(char[][] board) {
		int row = board.length;
		int col = board[0].length;
		UnionFind2 uf = new UnionFind2(board);
		for (int j = 1; j < col; j++) {
			if (board[0][j - 1] == '1' && board[0][j] == '1') {
				uf.union(0, j - 1, 0, j);
			}
		}
		for (int i = 1; i < row; i++) {
			if (board[i - 1][0] == '1' && board[i][0] == '1') {
				uf.union(i - 1, 0, i, 0);
			}
		}
		for (int i = 1; i < row; i++) {
			for (int j = 1; j < col; j++) {
				if (board[i][j] == '1') {
					if (board[i][j - 1] == '1') {
						uf.union(i, j - 1, i, j);
					}
					if (board[i - 1][j] == '1') {
						uf.union(i - 1, j, i, j);
					}
				}
			}
		}
		return uf.sets();
	}

	public static class UnionFind2 {
		private int[] parent;
		private int[] size;
		private int[] help;
		//列号
		private int col;
		private int sets;

		public UnionFind2(char[][] board) {
			//列数
			col = board[0].length;
			//行数
			int row = board.length;
			sets = 0;
			//长度准备行成列这么多个
			int len = row * col;
			//parent
			parent = new int[len];
			size = new int[len];
			help = new int[len];
			for (int r = 0; r < row; r++) {
				for (int c = 0; c < col; c++) {
					if (board[r][c] == '1') {
						//如果二维数组中某个位置是1,那么则算出这个位置的index
						int i = index(r, c);
						//同时让它的代表节点初始化为自己的节点
						parent[i] = i;
						//标识index位置的点的集合的size为1
						size[i] = 1;
						//总的集合数为1
						sets++;
					}
				}
			}
		}

		// (r,c) -> i 在r行c列,换算出下标是啥
		private int index(int r, int c) {
			return r * col + c;
		}

		// 原始位置 -> 下标(传入参数)
		private int find(int i) {
			int hi = 0;
			while (i != parent[i]) {
				help[hi++] = i;
				i = parent[i];
			}
			for (hi--; hi >= 0; hi--) {
				parent[help[hi]] = i;
			}
			return i;
		}

		public void union(int r1, int c1, int r2, int c2) {
			//合并两个节点,分别对应不同的(r1,c1)(r2,c2)可以求出不同的
			//index,然后通过index去找他们的father代表节点,找到之后则合并
			int i1 = index(r1, c1);
			int i2 = index(r2, c2);
			int f1 = find(i1);
			int f2 = find(i2);
			//如果父不是一个,则合并
			if (f1 != f2) {
				if (size[f1] >= size[f2]) {
					size[f1] += size[f2];
					parent[f2] = f1;
				} else {
					size[f2] += size[f1];
					parent[f1] = f2;
				}
				sets--;
			}
		}

		public int sets() {
			return sets;
		}

	}

岛问题2

A 2d grid map of m rows and n columns is initially filled with water. We may perform an addLand operation which turns the water at position (row, col) into a land. Given a list of positions to operate, count the number of islands after each addLand operation. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example:

Given m = 3, n = 3, positions = [[0,0], [0,1], [1,2], [2,1]].
Initially, the 2d grid grid is filled with water. (Assume 0 represents water and 1 represents land).

0 0 0
0 0 0
0 0 0
Operation #1: addLand(0, 0) turns the water at grid[0][0] into a land.

1 0 0
0 0 0 Number of islands = 1
0 0 0
Operation #2: addLand(0, 1) turns the water at grid[0][1] into a land.

1 1 0
0 0 0 Number of islands = 1
0 0 0
Operation #3: addLand(1, 2) turns the water at grid[1][2] into a land.

1 1 0
0 0 1 Number of islands = 2
0 0 0
Operation #4: addLand(2, 1) turns the water at grid[2][1] into a land.

1 1 0
0 0 1 Number of islands = 3
0 1 0
We return the result as an array: [1, 1, 2, 3]

Challenge:

Can you do it in time complexity O(k log mn), where k is the length of the positions?

	public static List<Integer> numIslands21(int m, int n, int[][] positions) {
		UnionFind1 uf = new UnionFind1(m, n);
		List<Integer> ans = new ArrayList<>();
		for (int[] position : positions) {
			ans.add(uf.connect(position[0], position[1]));
		}
		return ans;
	}

	public static class UnionFind1 {
		private int[] parent;
		private int[] size;
		private int[] help;
		private final int row;
		private final int col;
		private int sets;

		public UnionFind1(int m, int n) {
			row = m;
			col = n;
			sets = 0;
			int len = row * col;
			parent = new int[len];
			size = new int[len];
			help = new int[len];
		}

		private int index(int r, int c) {
			return r * col + c;
		}

		private int find(int i) {
			int hi = 0;
			while (i != parent[i]) {
				help[hi++] = i;
				i = parent[i];
			}
			for (hi--; hi >= 0; hi--) {
				parent[help[hi]] = i;
			}
			return i;
		}

		private void union(int r1, int c1, int r2, int c2) {
			//检查是否越界
			if (r1 < 0 || r1 == row || r2 < 0 || r2 == row || c1 < 0 || c1 == col || c2 < 0 || c2 == col) {
				return;
			}
			int i1 = index(r1, c1);
			int i2 = index(r2, c2);
			//如果这两个有一个不为1则不能连接,直接返回
			if (size[i1] == 0 || size[i2] == 0) {
				return;
			}
			int f1 = find(i1);
			int f2 = find(i2);
			if (f1 != f2) {
				if (size[f1] >= size[f2]) {
					//大的集合合并小的集合
					size[f1] += size[f2];
					//f2的parent=f1
					parent[f2] = f1;
				} else {
					//反之则让f1的parent=f2
					size[f2] += size[f1];
					parent[f1] = f2;
				}
				sets--;
			}
		}

		public int connect(int r, int c) {
			int index = index(r, c);
			//size[index] = 0 表明是第一次空降的index
			//如果size[index]!=0 则表明是重复空降的index
			if (size[index] == 0) {
				//代表节点是自己
				parent[index] = index;
				//当前节点的集合大小为1
				size[index] = 1;
				sets++;
				//然后再和上下左右去连接
				union(r - 1, c, r, c);
				union(r + 1, c, r, c);
				union(r, c - 1, r, c);
				union(r, c + 1, r, c);
			}

			return sets;
		}

	}

时间复杂度,如果有k个1,每个小1都和上下左右连一下,每次连的过程在并查集里面都是O(1)的,有K个点那么时间复杂度为O(K),初始化为O(MN),所以复杂度为O(MN)+O(K)

如果M是100亿,N是10亿,但是K只有5个,那么初始化就会浪费很多时间,那么如何优化?

	//如果m*n比较大,会经历很重的初始化,而k比较小,怎么优化的方法
	public static List<Integer> numIslands22(int m, int n, int[][] positions) {
		UnionFind2 uf = new UnionFind2();
		List<Integer> ans = new ArrayList<>();
		for (int[] position : positions) {
			ans.add(uf.connect(position[0], position[1]));
		}
		return ans;
	}

	//通过字符串标识位置如(17,1009)标识为“17_1009”
	public static class UnionFind2 {
		private HashMap<String, String> parent;
		private HashMap<String, Integer> size;
		private ArrayList<String> help;
		private int sets;

		public UnionFind2() {
			parent = new HashMap<>();
			size = new HashMap<>();
			help = new ArrayList<>();
			sets = 0;
		}

		private String find(String cur) {
			while (!cur.equals(parent.get(cur))) {
				help.add(cur);
				cur = parent.get(cur);
			}
			for (String str : help) {
				parent.put(str, cur);
			}
			help.clear();
			return cur;
		}

		private void union(String s1, String s2) {
			if (parent.containsKey(s1) && parent.containsKey(s2)) {
				String f1 = find(s1);
				String f2 = find(s2);
				if (!f1.equals(f2)) {
					int size1 = size.get(f1);
					int size2 = size.get(f2);
					String big = size1 >= size2 ? f1 : f2;
					String small = big == f1 ? f2 : f1;
					parent.put(small, big);
					size.put(big, size1 + size2);
					sets--;
				}
			}
		}

		public int connect(int r, int c) {
			String key = String.valueOf(r) + "_" + String.valueOf(c);
			if (!parent.containsKey(key)) {
				//自己是自己的代表节点
				parent.put(key, key);
				//集合大小为1
				size.put(key, 1);
				//集合大小加1
				sets++;
				String up = String.valueOf(r - 1) + "_" + String.valueOf(c);
				String down = String.valueOf(r + 1) + "_" + String.valueOf(c);
				String left = String.valueOf(r) + "_" + String.valueOf(c - 1);
				String right = String.valueOf(r) + "_" + String.valueOf(c + 1);
				union(up, key);
				union(down, key);
				union(left, key);
				union(right, key);
			}
			return sets;
		}

	}

岛问题(扩展)

如果matrix极大,设计一种可行的并行计算方案
如何通过一种分治的思想去并行处理这个问题。
首先将整个岛屿一分为2【下图黑色加粗线部分】,然后通过感染或并查集算法将各个岛屿标识为2,然后如下图左边部分划分了两个岛屿,
分别为A和B,然后再标记左边岛屿的边界节点他们的代表节点为谁(是通过谁感染而来的),那么边界的四个点分别是A,B,A,B感染来的。
在这里插入图片描述
同理再标记右边部分:
在这里插入图片描述
如上求出了四个岛屿,然后再去掉连接线,然后考虑连通性的事情,
然后先判断A和C是不是一个集合(本来是一个集合的但是因为联通性的原因导致不是一个集合了,所以应该将A和C合在一起,并将岛屿数量减一),同理然后判断B和C也是一个集合,岛屿数量减一,然后再判断B和D也是一个集合,岛屿数量减一,最后A和D本来就是一个集合了,因为前面的步骤将ABCD合并在一起了则岛屿数量不用减一。【查两侧都为2的时候,如果它已经合并到一起了说明联通性已经算过了,不需要减去这个1,如果没有算到一起则需要减这个1,然后最后的结果为整个matrix上面的岛屿】

那么对于大的地图,统计湖泊,则可以将地图划分为多个小块,然后每个小块分配一个线程跑,每个小块都搜集四个边界,在合并的过程中只做并查集的合并【并查集可以在一个大内存上面供所有CPU使用】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值