本文为转载文章,原文链接:https://blog.csdn.net/qq_42011541/article/details/83378709
并查集
引子
一开始有n伙山贼, 他们各自为营,但是他们都是有野心的
第3伙强盗,打下了第5伙强盗,第5伙强盗的老大就是第3伙强盗
然后第7伙强盗一看想要让第5伙强盗成为伙伴 打完第5伙还得打第3伙强盗,还不如直接打第3伙 于是第7伙强盗就打赢了第3伙强盗 然后第3伙和第5伙都归第7伙了
… …
接下来就是各自纷争 然后最后看还剩下了几伙强盗 并且每一伙强盗都是谁
1. union-find (简单并查集)
public class UnionFind {
//存储这几伙强盗的逻辑关系 数组下标i代表第i伙强盗,值代表他老大是谁
private int[] id;
//表示一共有几个强盗团伙
private int count;
//做初始化操作 ,N 代表一开始有几伙强盗
public UnionFind(int N) {
count = N;
id = new int[N];
for(int i = 0; i < N; i++) id[i] = i;
}
//获取强盗团伙的数量
public int getCount() {
return count;
}
//判断 p 和 q 这两伙强盗是不是一家的
public boolean connected(int p, int q) {
return find(p) == find(q);
}
//找到第p伙强盗的老大
public int find(int p) {
return id[p];
}
//联合两伙强盗
public void union(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
for(int i = 0; i <lt; id.length; i++)
if(id[i] == pRoot) id[i] = qRoot;
count--;
}
}
好了,让我们分析一下时间复杂度。
最主要就是在这个union()的方法上,每一次的合并都需要遍历一次这个数组 合并n次就需要O(n^2)的时间复杂度,但是这个现实的应用可能处理的n是几百万甚至几千万几亿的时候 这个时间复杂度的开销可能就有点问题了
2. quick-union (优化的并查集)
public int find(int p) {
while(p != id[p]) p = id[p];
return p;
}
public void union(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
count--;
}
迭代遍历的是什么? id[p]表示第p伙强盗的老大,只要老大不是自己就寻找真正的老大
要注意这里你可以有点误区 ,因为其实我们的union() 方法也改了这次做的并不是统一老大。上一个版本,我们3干掉了5,3就是5的老大,然后7干掉了3,3的老大和5的老大都变成了7。这次我们是3干掉了5 ,3就是5的老大, 7干掉了3 ,3的老大是7 ,5的老大还是3,所以7是5的老大的老大。
那么有人问这你不就是把循环写到find里了吗?有什么区别.
区别就是第一个真正的遍历了所有数组,而第二个可能连接起来的也就是几个、其实可以表示成一颗树
3. 加权值quick-union(处理了2的最坏情况)
但是2.会出现这样一种情况
4伙强盗 分别0 1 2 3
然后联合的顺序是
0 1
0 2
0 3
你会发现这样的情况遍历还是很多 ,树的高度呈线性增长。我们在实际的应用只关心两者联合在一起了,也就是两个团伙结盟在一起的问题,所以我们何不让小团伙依附于大团伙呢?
public class WeightedQuickUnion {
private int[] id;
private int count;
private int[] sz;
public WeightedQuickUnion(int N) {
count = N;
id = new int[N];
sz = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
sz[i] = 1;//这个代表各个强盗有几个小弟
}
}
public int getCount() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
while(p != id[p]) p = id[p];
return p;
}
public void union(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
//如果p团伙的强盗数量小于q团伙强盗数量 就让p团伙的真正老大变成q团伙的老大
//并且q团伙的数量扩增,也就是加上p老大的小弟数
if(sz[pRoot] < sz[qRoot])
id[pRoot] = qRoot; sz[qRoot] += sz[pRoot];
else
id[qRoot] = pRoot; sz[pRoot] += sz[qRoot];
count--;
}
}
4.路径压缩加权值quick-union
你想一想,如果你是那个小弟,你有了个老大,你的老大还有老大,那个真正的老大能让你的小弟还有小弟吗?
我都把你打掉了,你还有小弟这可不行。
那我们能不能在找老大时候找到了以后都挨个告诉他们真正的老大变成谁了呢,没问题。
所以这就是路径压缩所在了
你想我们上一个版本去找老大的时候,还要一层一层的寻找,是不是很麻烦呢? 如果每一次找到了以后,小弟是老大就变成了真正的老大,那么我们每次去寻找的时候是不是只需要找一次
3是5的老大,然后 ip[5] = 3,来了个7干掉了3 ip[3] = 7; 再来了一个人需要打5的时候,去找老大,找到3后,发现3存的是7,那我自己也存7,那下次再访问的时候是不是直接就是7了,就直接跳过3,如果还有更多的比如再来个9,9干掉了7 ip[7] = 9,继续找5的时候发现,我的老大7怎么存的是9,那我也是9,下下次,直接找7,是不是3和7都跳过了
所以这就是路径压缩的魅力所在
实现方法也简单 其他的不变修改find就行
这里递归调用 层层返回,最后路径之上的所有强盗的老大,都是最后找到的那个老大了
是不是很Nice呢?
public int find(int p) {
if(p != id[p]) id[p] = find(id[p]);
return id[p];
}
时间复杂度
算法 | 构造函数 | union() | find() |
---|---|---|---|
union-find算法 | O(n) | O(n) | O(1) |
quick-union算法 | O(n) | 树的高度 | 树的高度 |
加权quick-union算法 | O(n) | O(lgn) | O(lgn) |
路径压缩的加权quick-union算法 | O(n) | 非常接近O(1) | 非常接近O(1) |
理想情况 | O(n) | O(1) | O(1) |
应用
无向图中连通分量的数目
最长连续序列
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。
class Solution {
Map<Integer, Integer> map = new HashMap<>();
Map<Integer, Integer> sz = new HashMap<>();
public int find(int p){
if(p!=map.get(p)){
int v = find(map.get(p));
map.put(p, v);
}
return map.get(p);
}
public int union(int x, int y){
x = find(x);
y = find(y);
if(x==y)
return sz.get(x);
map.put(x, y);
sz.put(y, sz.get(x) + sz.get(y));
return sz.get(y);
}
public int longestConsecutive(int[] nums) {
if(nums==null || nums.length==0)
return 0;
for(int v :nums){
map.put(v, v);
sz.put(v, 1);
}
int res = 1;
for(int v :nums){
if(map.containsKey(v-1))
res = Math.max(res, union(v-1,v));
if(map.containsKey(v+1))
res = Math.max(res, union(v+1,v));
}
return res;
}
}
岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。输入:
[
[‘1’,‘1’,‘1’,‘1’,‘0’],
[‘1’,‘1’,‘0’,‘1’,‘0’],
[‘1’,‘1’,‘0’,‘0’,‘0’],
[‘0’,‘0’,‘0’,‘0’,‘0’]
]
输出: 1
class Solution {
class UnionFind{
int[] parent;
int[] sz;
int count;
UnionFind(char[][] grid){
int m = grid.length;
int n = grid[0].length;
parent = new int[m*n];
sz = new int[m*n];
count =0;
for(int i=0; i<m; i++){
for(int j = 0; j<n; j++){
if(grid[i][j]=='1'){
parent[i*n+j] = i*n +j;
count++;
}
sz[i*n+j] = 0;
}
}
}
public int getCount(){
return count;
}
public int find(int v){
if(v!=parent[v])
parent[v] = find(parent[v]);
return parent[v];
}
public void union(int v1, int v2){
v1 = find(v1);
v2 = find(v2);
if(v1==v2)
return ;
if(sz[v1]<sz[v2]){
parent[v1] = v2;
sz[v2] +=sz[v1];
}else{
parent[v2] = v1;
sz[v1] +=sz[v2];
}
count--;
}
}
public int numIslands(char[][] grid) {
if(grid==null || grid.length==0 || grid[0].length==0)
return 0;
UnionFind uf = new UnionFind(grid);
int m = grid.length;
int n = grid[0].length;
for(int i =0; i<m; i++){
for(int j = 0; j<n; j++){
if(grid[i][j]=='1'){
if(i-1>=0 && grid[i-1][j]=='1')
uf.union(i*n+j, i*n-n+j);
if(i+1<m && grid[i+1][j]=='1')
uf.union(i*n+j, i*n+n+j);
if(j-1>=0 && grid[i][j-1]=='1')
uf.union(i*n+j, i*n-1+j);
if(j+1<n && grid[i][j+1]=='1')
uf.union(i*n+j, i*n+1+j);
}
}
}
return uf.getCount();
}
}
除法求值(*)
给出方程式 A / B = k, 其中 A 和 B 均为用字符串表示的变量, k 是一个浮点型数字。根据已知方程式求解问题,并返回计算结果。如果结果不存在,则返回 -1.0。
输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。输入:equations = [[“a”,“b”],[“b”,“c”]], values = [2.0,3.0], queries = [[“a”,“c”],[“b”,“a”],[“a”,“e”],[“a”,“a”],[“x”,“x”]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
给定:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
返回:[6.0, 0.5, -1.0, 1.0, -1.0 ]
婴儿名字
每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。
在结果列表中,选择字典序最小的名字作为真实名字。输入:names = [“John(15)”,“Jon(12)”,“Chris(13)”,“Kris(4)”,“Christopher(19)”], synonyms = ["(Jon,John)","(John,Johnny)","(Chris,Kris)","(Chris,Christopher)"]
输出:[“John(27)”,“Chris(36)”]
class Solution {
Map<String, String> parent = new HashMap<>();
Map<String, Integer> sz = new HashMap<>();
int count =0;
public String find(String name){
if(!parent.get(name).equals(name))
parent.put(name, find(parent.get(name)));
return parent.get(name);
}
public void union(String nma, String nmb){
String roota = find(nma);
String rootb = find(nmb);
int c = roota.compareTo(rootb);
if(c==0)
return;
if(c>0){
parent.put(roota, rootb);
sz.put(rootb, sz.get(roota) + sz.get(rootb));
}
else{
parent.put(rootb, roota);
sz.put(roota, sz.get(roota) + sz.get(rootb));
}
count--;
}
public String[] trulyMostPopular(String[] names, String[] synonyms) {
count += names.length;
for(String name: names){
String[] info = name.split("\\(");
parent.put(info[0], info[0]);
sz.put(info[0], Integer.parseInt(info[1].substring(0,info[1].length()-1)));
}
for(String pair: synonyms){
String[] info = pair.split(",");
String nma = info[0].substring(1);
String nmb = info[1].substring(0, info[1].length()-1);
if(parent.containsKey(nma) && parent.containsKey(nmb))
union(nma, nmb);
}
String[] res = new String[count];
int id =0;
Set<Map.Entry<String, String>> set = parent.entrySet();
for(Map.Entry<String, String> entry : set){
String key = entry.getKey();
String p =entry.getValue();
if(key.equals(p)){
res[id++] = key + "(" + sz.get(key) + ")";
}
}
return res;
}
}
以图判树
给定从 0 到 n-1 标号的 n 个结点,和一个无向边列表(每条边以结点对来表示),请编写一个函数用来判断这些边是否能够形成一个合法有效的树结构。
输入: n = 5, 边列表 edges = [[0,1], [0,2], [0,3], [1,4]]
输出: true输入: n = 5, 边列表 edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
输出: false
- 一个图是树的条件:
- 连通
- 无环;结点数 = 边数 +1
- **连通:**从某一结点出发可以遍历到所有结点
- **无环:**以深度或广度遍历数的边数<总边数,则有环
- 并查集
最后只有一个集合则连通;若某一条边的两个顶点不在同一集合中,则合并;若在,则有环!
class Solution {
int count;
int[] parent;
int[] sz;
public int find(int v){
if(v!=parent[v])
parent[v] = find(parent[v]);
return parent[v];
}
public boolean union(int x, int y){
x = find(x);
y = find(y);
if(x==y)
return false;
if(sz[x]<sz[y]){
parent[x] = y;
sz[y] +=sz[x];
}else{
parent[y] = x;
sz[x] += sz[y];
}
count--;
return true;
}
public boolean validTree(int n, int[][] edges) {
count = n;
parent = new int[n];
sz = new int[n];
for(int i=0; i<n; i++){
parent[i] = i;
sz[i] = 1;
}
for(int i=0; i< edges.length; i++){
boolean b = union(edges[i][0], edges[i][1]);
if(!b)
return b;
}
return count==1;
}
}
冗余连接
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。示例 1:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
class Solution {
int[] res = new int[2];
int[] parent;
public int find(int x){
if(x!=parent[x])
parent[x] = find(parent[x]);
return parent[x];
}
public void union(int x, int y){
int rx = find(x);
int ry = find(y);
if(rx==ry){
res[0] = x;
res[1] = y;
}else
parent[rx] = parent[ry];
}
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
parent = new int[n+1];
for(int i=0; i<=n; i++){
parent[i] = i;
}
for(int i =0; i<n; i++){
union(edges[i][0], edges[i][1]);
}
return res;
}
}
冗余连接2(*)
在本问题中,有根树指满足以下条件的有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。每一个节点只有一个父节点,除了根节点没有父节点。
输入一个有向图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。 每一个边 的元素是一对 [u, v],用以表示有向图中连接顶点 u 和顶点 v 的边,其中 u 是 v 的一个父节点。
返回一条能删除的边,使得剩下的图是有N个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
被围绕的区域
给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
class Solution {
int m,n;
int[] parent;
boolean[] inBorder;
public int find(int x){
if(x!=parent[x])
parent[x] = find(parent[x]);
return parent[x];
}
public void union(int x, int y){
int rootx = find(x);
int rooty = find(y);
if(rootx==rooty)
return;
parent[rootx] = rooty;
inBorder[rooty] |=inBorder[rootx];
}
public void solve(char[][] board) {
if(board==null ||board.length==0 ||board[0].length==0)
return;
m = board.length;
n = board[0].length;
parent = new int[m*n];
inBorder = new boolean[m*n];
for(int i=0; i<m; i++)
for(int j=0; j<n; j++){
parent[i*n+j] = i*n+j;
if(i==0 || i==m-1 || j==0 || j==n-1)
inBorder[i*n+j] = true;
else
inBorder[i*n+j] = false;
}
for(int i=0; i<m; i++)
for(int j=0; j<n; j++)
if(board[i][j]=='O'){
if(i-1>=0 && board[i-1][j]=='O')
union(i*n+j, i*n-n+j);
if(i+1<m && board[i+1][j]=='O')
union(i*n+j, i*n+n+j);
if(j-1>=0 && board[i][j-1]=='O')
union(i*n+j, i*n-1+j);
if(j+1<n && board[i][j+1]=='O')
union(i*n+j, i*n+1+j);
}
for(int i=0; i<m; i++)
for(int j=0; j<n; j++){
int v = i*n+j;
if(board[i][j]=='O' && !inBorder[find(v)])
board[i][j] = 'X';
}
}
}
朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出:2
解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回 2 。
class Solution {
int[] parent;
int count;
public int find(int x){
if(x != parent[x])
parent[x] = find(parent[x]);
return parent[x];
}
public void union(int p, int q){
int rootx = find(p);
int rooty = find(q);
if(rootx==rooty)
return;
parent[rootx] = rooty;
count--;
}
public int findCircleNum(int[][] M) {
if(M==null || M.length==0 ||M[0].length==0)
return 0;
int n = M.length;
count = n;
parent = new int[n];
for(int i= 0; i<n; i++)
parent[i] = i;
for(int i= 0; i<n; i++){
for(int j = i+1; j<n; j++)
if(M[i][j]==1)
union(i,j);
}
return count;
}
}