并查集(Disjoint Set Union)

625 阅读4分钟

1.并查集定义

并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。

2.代码模板

2.1 朴素并查集

int p[N]; //存储每个点的祖宗节点

// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;

// 合并a和b所在的两个集合:
p[find(a)] = find(b);

2.2 维护size的并查集

int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    size[i] = 1;
}

// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);

2.3 维护到祖宗节点距离的并查集

int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

// 返回x的祖宗节点
int find(int x)
{
   if (p[x] != x)
   {
       int u = find(p[x]);
       d[x] += d[p[x]];
       p[x] = u;
   }
   return p[x];
}

// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
   p[i] = i;
   d[i] = 0;
}

// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

3.具体例题

3.1 合并集合
原题链接

一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:

  1. “M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. “Q a b”,询问编号为a和b的两个数是否在同一个集合中;
    输入格式
    第一行输入整数n和m。
    接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种。
    输出格式
    对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
    每个结果占一行。

思路:根据并查集的特点,将输入的数据放入并查集中,然后根据查询指令来输出结果。

import java.util.*;

class Main {
	static int n,m;
	static final int N = 100000;
	static int[] p = new int[N];
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		m = sc.nextInt();
		while (m-- > 0) {
			String str = sc.next();
			int a = sc.nextInt();
			int b = sc.nextInt();
			if (str.equals("M")){
				merge(a,b);
			}else {
				query(a,b);
			}
		}
	}
	private static int find(int x) {
		if (p[x] != x) {
			p[x] = find(p[x]);
		}
		return p[x];
	}

	private static void merge(int a,int b) {
		p[find(a)] = find(b);
	}

	private static void query(int a,int b) {
		if (find(a) == find(b)){
			System.out.println("Yes");
		}else{
			System.out.println("No");
		}
	}
}

3.2 Leetcode990
原题链接

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或"a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回true,否则返回 false。

解法: 构建一个并查集,然后将equations数组里的方程merge一下放入并查集,如果是==则看作是连接两个节点的边,那么由于表示相等关系的等式具有传递性,a==b和b==c可以推出a==c。也就是所有相等的变量可以有相同的祖宗节点。如果是!=,同一个不等式中的变量不能属于一条边上,所以查找他们分别的祖宗节点,如果相等则为矛盾,输出false。

代码

class Solution {
    static int[] p = new int[26];
    public boolean equationsPossible(String[] equations) {
        for (int i = 0;i < 26;i++) {
            p[i] = i;
        }
        for (int k = 0;k < equations.length;k++) {
            String str = equations[k];
            int i = str.charAt(0) - 'a';
            int j = str.charAt(3) - 'a';
            if (str.charAt(1) == '=') {
                merge(i,j);
            }
        }
        for (int k = 0;k < equations.length;k++) {
            String str = equations[k];
            int i = str.charAt(0) - 'a';
            int j = str.charAt(3) - 'a';
            if (str.charAt(1) == '!') {
                if (find(i) == find(j)) return false;
            }
        }
        return true;
    }

    public static void merge(int i,int j) {
        p[find(i)] = find(j);
    }

    public static int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
}