并查集(Union-find)及经典问题
解决的是连通性问题
并查集是一种抽象画很高的数据结构, 常用于描述集合,经常用于解决此类问题:某个元素是否属于某个集合,或者 某个元素 和 另一个元素是否同属于一个集合
Union-find实现
public class UnionFind {
int[] nums;//保存各个节点的父节点---其实就是该节点的前一个链接节点
int[] size;//保存节点下子节点的数量---权重
int count;//还剩多少个节点未连通
public UnionFind(int n){//初始化
this.nums = new int[n];
this.size = new int[n];
this.count = n;
for(int i = 0;i < n;i++){
this.nums[i] = i;//每个节点的父节点都是其本身
this.size[i] = 1;//每个节点的权重都是1
}
}
//寻找一个节点的顶层父节点
public int find(int a){
int num = a;
while(nums[num] != num){//当他的父节点不是他
nums[num] = nums[nums[num]];//这一步是为了压缩路径,画图就可以很好地理解了。
num = nums[num];//走向该节点的父节点,继续找顶层的父节点
}
return num;
}
//判断两个节点是否连通
public boolean union(int a,int b){
return find(a) == find(b);//两个节点的父节点一致,就说明已经连通
}
//连通两个节点
public void merge(int a,int b){
int num_a = find(a);
int num_b = find(b);
if(num_a == num_b) return;
count--;
//判断谁的权重小,谁就放在下面
if(size[num_a] > size[num_b]){
nums[num_b] = num_a;
size[num_a] += size[num_b];
}else{
nums[num_a] = num_b;
size[num_b] += size[num_a];
}
}
public int getCount(){
return this.count;
}
}
测验:
public class Main{
public static void main(String args[]){
UnionFind unionFind = new UnionFind(10);
unionFind.merge(0,1);
unionFind.merge(1,2);
System.out.println("0->2:" + (unionFind.union(0,2) ? "已连通" : "未连通"));
unionFind.merge(3,4);
unionFind.merge(4,7);
System.out.println("3->7:" + (unionFind.union(3,7) ? "已连通" : "未连通"));
System.out.println("1->7:" + (unionFind.union(1,7) ? "已连通" : "未连通"));
}
}
0->2:已连通
3->7:已连通
1->7:未连通
基础题目
LeetCode547. 省份数量
class Solution {
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
UnionFind unionFind = new UnionFind(n);//需要粘过来UnionFind的代码,太长了所以这里就不粘贴了
for(int i = 0;i < n;i++){
for(int j = 0;j < i;j++){//i < j是因为,该矩阵是对阵矩阵,避免重复计算
if(isConnected[i][j] == 1) unionFind.merge(i,j);
}
}
int res = 0;
for(int i = 0;i < n;i++){
if(unionFind.find(i) == i) res++;
}
return res;
}
}
LeetCode200. 岛屿数量
当然这道题用并查集不一定是最好的方法
class Solution {
public int numIslands(char[][] grid) {
int n = grid.length;
int m = grid[0].length;
UnionFind unionFind = new UnionFind(n * m);//需要粘过来UnionFind的代码,太长了所以这里就不粘贴了
for(int i = 0;i < n;i++){
for(int j = 0;j < m;j++){
if(grid[i][j] == '0') continue;
if(i - 1 >= 0 && grid[i - 1][j] == '1') unionFind.merge(i * m + j,(i - 1) * m + j);
if(j - 1 >= 0 && grid[i][j - 1] == '1') unionFind.merge(i * m + j,i * m + j - 1);
}
}
int ans = 0;
for(int i = 0; i < n;i++){
for(int j = 0;j < m;j++){
if(grid[i][j] == '1' && unionFind.find(i * m + j) == i * m + j) ans += 1;
}
}
return ans;
}
}
LeetCode990. 等式方程的可满足性
class Solution {
public boolean equationsPossible(String[] equations) {
UnionFind uf = new UnionFind(26);
for(String eq:equations){
if(eq.charAt(1) == '='){
char x = eq.charAt(0);
char y = eq.charAt(3);
uf.union(x - 'a',y - 'a');
}
}
for(String eq:equations){
if(eq.charAt(1) == '!'){
char x = eq.charAt(0);
char y = eq.charAt(3);
if(uf.connect(x - 'a',y - 'a')){
return false;
}
}
}
return true;
}
}
LeetCode684. 冗余连接
class Solution {
public int[] findRedundantConnection(int[][] edges) {
UnionFind unionFind = new UnionFind(edges.length + 1);
int[] num = new int[2];
for(int[] n : edges){
int a = n[0];
int b = n[1];
if(unionFind.find(a) == unionFind.find(b)) return n;
unionFind.merge(a,b);
}
return num;
}
}
//下面是并查集代码
class UnionFind {
int[] nums;//保存各个节点的父节点---其实就是该节点的前一个链接节点
int[] size;//保存节点下子节点的数量---权重
int count;//还剩多少个节点未连通
public UnionFind(int n){//初始化
this.nums = new int[n];
this.size = new int[n];
this.count = n;
for(int i = 0;i < n;i++){
this.nums[i] = i;//每个节点的父节点都是其本身
this.size[i] = 1;//每个节点的权重都是1
}
}
//寻找一个节点的顶层父节点
public int find(int a){
int num = a;
while(nums[num] != num){//当他的父节点不是他
nums[num] = nums[nums[num]];//这一步是为了压缩路径,画图就可以很好地理解了。
num = nums[num];//走向该节点的父节点,继续找顶层的父节点
}
return num;
}
//判断两个节点是否连通
public boolean union(int a,int b){
return find(a) == find(b);//两个节点的父节点一致,就说明已经连通
}
//连通两个节点
public void merge(int a,int b){
int num_a = find(a);
int num_b = find(b);
if(num_a == num_b) return;
count--;
//判断谁的权重小,谁就放在下面
if(size[num_a] > size[num_b]){
nums[num_b] = num_a;
size[num_a] += size[num_b];
}else{
nums[num_a] = num_b;
size[num_b] += size[num_a];
}
}
public int getCount(){
return this.count;
}
}
LeetCode1319. 连通网络的操作次数
class Solution {
//有n个独立的集合,需要的操作数量是n - 1次
//如果电缆的数量小于n - 1。那么电缆的数量不够,返回-1
public int makeConnected(int n, int[][] connections) {
if(connections.length < n - 1) return -1;
UnionFind unionFind = new UnionFind(n);
for(int[] connection : connections){
int a = connection[0];
int b = connection[1];
unionFind.merge(a,b);
}
int ans = 0;
for(int i = 0;i < n;i++){
if(unionFind.find(i) == i) ans += 1;
}
return ans - 1;
}
}
//下面是并查集代码
class UnionFind {
int[] nums;//保存各个节点的父节点---其实就是该节点的前一个链接节点
int[] size;//保存节点下子节点的数量---权重
int count;//还剩多少个节点未连通
public UnionFind(int n){//初始化
this.nums = new int[n];
this.size = new int[n];
this.count = n;
for(int i = 0;i < n;i++){
this.nums[i] = i;//每个节点的父节点都是其本身
this.size[i] = 1;//每个节点的权重都是1
}
}
//寻找一个节点的顶层父节点
public int find(int a){
int num = a;
while(nums[num] != num){//当他的父节点不是他
nums[num] = nums[nums[num]];//这一步是为了压缩路径,画图就可以很好地理解了。
num = nums[num];//走向该节点的父节点,继续找顶层的父节点
}
return num;
}
//判断两个节点是否连通
public boolean union(int a,int b){
return find(a) == find(b);//两个节点的父节点一致,就说明已经连通
}
//连通两个节点
public void merge(int a,int b){
int num_a = find(a);
int num_b = find(b);
if(num_a == num_b) return;
count--;
//判断谁的权重小,谁就放在下面
if(size[num_a] > size[num_b]){
nums[num_b] = num_a;
size[num_a] += size[num_b];
}else{
nums[num_a] = num_b;
size[num_b] += size[num_a];
}
}
public int getCount(){
return this.count;
}
}
LeetCode128. 最长连续序列
class Solution {
public int longestConsecutive(int[] nums) {
HashMap<Integer,Integer> map = new HashMap<>();
UnionFind unionFind = new UnionFind(nums.length);
for(int i = 0;i < nums.length;i++){
int x = nums[i];
if(map.containsKey(x)) continue;
if(map.containsKey(x - 1)) unionFind.merge(i,map.get(x - 1));
if(map.containsKey(x + 1)) unionFind.merge(i,map.get(x + 1));
map.put(x,i);
}
int ans = 0;
for(int i = 0;i < nums.length;i++){
if(unionFind.find(i) == i && unionFind.size[i] > ans) ans = unionFind.size[i];
}
return ans;
}
}
//下面是并查集代码
class UnionFind {
int[] nums;//保存各个节点的父节点---其实就是该节点的前一个链接节点
int[] size;//保存节点下子节点的数量---权重
int count;//还剩多少个节点未连通
public UnionFind(int n){//初始化
this.nums = new int[n];
this.size = new int[n];
this.count = n;
for(int i = 0;i < n;i++){
this.nums[i] = i;//每个节点的父节点都是其本身
this.size[i] = 1;//每个节点的权重都是1
}
}
//寻找一个节点的顶层父节点
public int find(int a){
int num = a;
while(nums[num] != num){//当他的父节点不是他
nums[num] = nums[nums[num]];//这一步是为了压缩路径,画图就可以很好地理解了。
num = nums[num];//走向该节点的父节点,继续找顶层的父节点
}
return num;
}
//判断两个节点是否连通
public boolean union(int a,int b){
return find(a) == find(b);//两个节点的父节点一致,就说明已经连通
}
//连通两个节点
public void merge(int a,int b){
int num_a = find(a);
int num_b = find(b);
if(num_a == num_b) return;
count--;
//判断谁的权重小,谁就放在下面
if(size[num_a] > size[num_b]){
nums[num_b] = num_a;
size[num_a] += size[num_b];
}else{
nums[num_a] = num_b;
size[num_b] += size[num_a];
}
}
public int getCount(){
return this.count;
}
}
LeetCode947. 移除最多的同行或同列石头
class Solution {
public int removeStones(int[][] stones) {
int n = stones.length;
UnionFind unionFind = new UnionFind(n);
HashMap<Integer,Integer> x_index = new HashMap<>();
HashMap<Integer,Integer> y_index = new HashMap<>();
for(int i = 0;i < n;i++){
int x = stones[i][0];
int y = stones[i][1];
if(x_index.containsKey(x)) unionFind.merge(i,x_index.get(x));
if(y_index.containsKey(y)) unionFind.merge(i,y_index.get(y));
x_index.put(x,i);
y_index.put(y,i);
}
int ans = 0;
for(int i = 0;i < n;i++){
if(unionFind.find(i) == i) ans += 1;
}
return n - ans;
}
}
//下面是并查集代码
class UnionFind {
int[] nums;//保存各个节点的父节点---其实就是该节点的前一个链接节点
int[] size;//保存节点下子节点的数量---权重
int count;//还剩多少个节点未连通
public UnionFind(int n){//初始化
this.nums = new int[n];
this.size = new int[n];
this.count = n;
for(int i = 0;i < n;i++){
this.nums[i] = i;//每个节点的父节点都是其本身
this.size[i] = 1;//每个节点的权重都是1
}
}
//寻找一个节点的顶层父节点
public int find(int a){
int num = a;
while(nums[num] != num){//当他的父节点不是他
nums[num] = nums[nums[num]];//这一步是为了压缩路径,画图就可以很好地理解了。
num = nums[num];//走向该节点的父节点,继续找顶层的父节点
}
return num;
}
//判断两个节点是否连通
public boolean union(int a,int b){
return find(a) == find(b);//两个节点的父节点一致,就说明已经连通
}
//连通两个节点
public void merge(int a,int b){
int num_a = find(a);
int num_b = find(b);
if(num_a == num_b) return;
count--;
//判断谁的权重小,谁就放在下面
if(size[num_a] > size[num_b]){
nums[num_b] = num_a;
size[num_a] += size[num_b];
}else{
nums[num_a] = num_b;
size[num_b] += size[num_a];
}
}
public int getCount(){
return this.count;
}
}
LeetCode1202. 交换字符串中的元素
class Solution {
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
if (pairs.size() == 0) {
return s;
}
// 第 1 步:将任意交换的结点对输入并查集
int len = s.length();
UnionFind unionFind = new UnionFind(len);
for (List<Integer> pair : pairs) {
int index1 = pair.get(0);
int index2 = pair.get(1);
unionFind.merge(index1, index2);
}
// 第 2 步:构建映射关系
char[] charArray = s.toCharArray();
// key:连通分量的代表元,value:同一个连通分量的字符集合(保存在一个优先队列中)
Map<Integer, PriorityQueue<Character>> hashMap = new HashMap<>(len);
for (int i = 0; i < len; i++) {
int root = unionFind.find(i);
// if (hashMap.containsKey(root)) {
// hashMap.get(root).offer(charArray[i]);
// } else {
// PriorityQueue<Character> minHeap = new PriorityQueue<>();
// minHeap.offer(charArray[i]);
// hashMap.put(root, minHeap);
// }
// 上面六行代码等价于下面一行代码,JDK 1.8 以及以后支持下面的写法
hashMap.computeIfAbsent(root, key -> new PriorityQueue<>()).offer(charArray[i]);
}
// 第 3 步:重组字符串
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < len; i++) {
int root = unionFind.find(i);
stringBuilder.append(hashMap.get(root).poll());
}
return stringBuilder.toString();
}
}
//下面是并查集代码
class UnionFind {
int[] nums;//保存各个节点的父节点---其实就是该节点的前一个链接节点
int[] size;//保存节点下子节点的数量---权重
int count;//还剩多少个节点未连通
public UnionFind(int n){//初始化
this.nums = new int[n];
this.size = new int[n];
this.count = n;
for(int i = 0;i < n;i++){
this.nums[i] = i;//每个节点的父节点都是其本身
this.size[i] = 1;//每个节点的权重都是1
}
}
//寻找一个节点的顶层父节点
public int find(int a){
int num = a;
while(nums[num] != num){//当他的父节点不是他
nums[num] = nums[nums[num]];//这一步是为了压缩路径,画图就可以很好地理解了。
num = nums[num];//走向该节点的父节点,继续找顶层的父节点
}
return num;
}
//判断两个节点是否连通
public boolean union(int a,int b){
return find(a) == find(b);//两个节点的父节点一致,就说明已经连通
}
//连通两个节点
public void merge(int a,int b){
int num_a = find(a);
int num_b = find(b);
if(num_a == num_b) return;
count--;
//判断谁的权重小,谁就放在下面
if(size[num_a] > size[num_b]){
nums[num_b] = num_a;
size[num_a] += size[num_b];
}else{
nums[num_a] = num_b;
size[num_b] += size[num_a];
}
}
public int getCount(){
return this.count;
}
}