并查集定义
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。
借鉴一个看到的故事
江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的帮派,通过两两之间的朋友关系串联起来。而不在同一个帮派的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?
我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物。这样,每个圈子就可以这样命名“中国同胞队”美国同胞队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。
但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样,想打一架得先问个几十年,饿都饿死了,受不了。这样一来,队长面子上也挂不住了,不仅效率太低,还有可能陷入无限循环中。
于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否是一个帮派的,至于他们是如何通过朋友关系相关联的,以及每个圈子内部的结构是怎样的,甚至队长是谁,都不重要了。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。
代码实现:
在代码中,通常使用一个 parent数组表示所有元素,数组下标表示具体每一个元素,每个数组值表示该元素对应的父节点(也就是它的上级),如果该元素是根节点或者是单独的节点,该元素的值为自己本身,如 parent[3] = 3
例子:
public class QuickFindUF {
private int[] parent = new int[5]; //定义长度为5的数组,表示每个元素
public void init() { //初始化数组,开始时,每个元素指向自己
for(int i = 0; i < parent.length; i++) {
parent[i] = i;
}
}
private int root(int i) { //找到节点i的根节点
while(i != parent[i]) {
i = parent[i];
}
return i;
}
public void union(int p, int q) { //快速合并p q两个节点:先找到两个根节点,然后将其中一个根节点指向另一个根节点
int proot = root(p);
int qroot = root(q);
parent[proot] = qroot;
}
public boolean connected(int p, int q) { //判断节点p和q是否相连
return root(p) == root(q);
}
public static void main(String[] args) {
init();
union(0, 2);
union(1, 4);
}
}
典型应用:
并查集常常用来判断在一个图中是否存在回路(是否可以生成树),以及用来判断图的联通性问题
leetcode 684. 冗余连接
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着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]
解释: 给定的无向图为:
1
/ \
2 - 3
示例 2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
| |
4 - 3
注意:
- 输入的二维数组大小在 3 到 1000。
//答案
class Solution {
public int[] parent;
public int[] findRedundantConnection(int[][] edges) {
parent = new int[edges.length + 1];
init(edges.length);
for(int i = 0; i < edges.length; i++) {
if(connected(edges[i][0], edges[i][1])) {
return edges[i];
} else {
union(edges[i][0], edges[i][1]);
}
}
return edges[edges.length];
}
private int root(int i) {
while(i != parent[i]) {
i = parent[i];
}
return i;
}
public boolean connected(int p, int q) {
return root(p) == root(q);
}
public void union(int p, int q) {
int proot = root(p);
int qroot = root(q);
parent[proot] = qroot;
}
public void init(int N) {
for(int i = 1; i <= N; i++) {
parent[i] = i;
}
}
}
leetcode 721. 账户合并
给定一个列表 accounts
,每个元素 accounts[i]
是一个字符串列表,其中第一个元素 accounts[i][0]
是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。
现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。
合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是按字符 ASCII 顺序排列的邮箱地址。账户本身可以以任意顺序返回。
示例 1:
输入:
accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]]
输出:
[["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'], ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]]
解释:
第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 "johnsmith@mail.com"。
第二个 John 和 Mary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。
可以以任何顺序返回这些列表,例如答案 [['Mary','mary@mail.com'],['John','johnnybravo@mail.com'],
['John','john00@mail.com','john_newyork@mail.com','johnsmith@mail.com']] 也是正确的。
//答案
class Solution {
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, Integer> email2index = new HashMap<String, Integer>();
Map<String, String> email2name = new HashMap<String, String>();
int count = 0;
for(List<String> account : accounts) {
String name = account.get(0);
int size = account.size();
for(int i = 1; i < size; i++) {
String email = account.get(i);
if(!email2index.containsKey(email)) {
email2index.put(email, count++);
email2name.put(email, name);
}
}
}
FindUF df = new FindUF();
df.init(count);
for(List<String> account : accounts) {
String firstEmail = account.get(1);
int firstIndex = email2index.get(firstEmail);
int size = account.size();
for(int i = 2; i < size; i++) {
String nextEmail = account.get(i);
int nextIndex = email2index.get(nextEmail);
df.union(firstIndex, nextIndex);
}
}
Map<Integer, List<String>> index2emails = new HashMap<Integer, List<String>>();
for(String email : email2index.keySet()) {
int index = df.root(email2index.get(email));
List<String> account = index2emails.getOrDefault(index, new ArrayList<String>());
account.add(email);
index2emails.put(index, account);
}
List<List<String>> merged = new ArrayList<List<String>>();
for(List<String> emails : index2emails.values()) {
Collections.sort(emails);
String name = email2name.get(emails.get(0));
List<String> account = new ArrayList<String>();
account.add(name);
account.addAll(emails);
merged.add(account);
}
return merged;
}
}
class FindUF {
int[] parent;
public void init(int n) {
parent = new int[n];
for(int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int root(int i) {
while(parent[i] != i) {
i = parent[i];
}
return i;
}
public void union(int p, int q) {
int p1 = root(p);
int q1 = root(q);
parent[q1] = p1;
}
public boolean isConnected(int p, int q) {
return root(q) == root(p);
}
}