高级数据结构之并查集
1.并查集介绍
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。
2.并查集核心方法
//有两种设置初始值方法
//1.全设置为-1,但不方便路径压缩, parent[i] = -1;
//2.设置为相应坐标值,parent[i] = i;
int[] parent;
public find(int x){
//查找x的树根
//优化
//路径压缩 自顶向下更新parent中的树根
}
public union(int x, int y){
//进行树根合并
//合并方法优化 扁平化合并
//1.设置一个rank[]数组,按秩合并
//2.设置size[]数组,按节点合并
}
如下代码判断是否有环,合并失败则有同一个树根证明有环
public class DisjoinSet {
static final int N = 6;
public int find(int x, int[] parent){
if(parent[x] != -1){
x = find(parent[x], parent);
}
return x;
}
public boolean union(int x, int y, int[] parent){
int x_root = find(x, parent);
int y_root = find(y, parent);
if(x_root == y_root)return false;
//x的根为y
parent[x_root] = y_root;
return true;
}
@Test
public void test(){
int[] parent = new int[N];
Arrays.fill(parent, -1);
int[][] matrix = {{0, 1}, {1, 2}, {1, 3}, {2, 5}, {3, 4}, {2, 4}};
int[][] edges = {{0, 1}, {1, 2}, {1, 3}, {2, 5}, {3, 4}};
// for(int i = 0; i < N; i++){
// int x = matrix[i][0];
// int y = matrix[i][1];
// if(!union(x, y, parent)){
// System.out.println("Cycle existed!");
// break;
// }
// }
for(int i = 0; i < N - 1; i++){
int x = edges[i][0];
int y = edges[i][1];
if(!union(x, y, parent)) {
System.out.println("Cycle existed!");
}
}
System.out.println("UnCycle");
}
3.优化
1.秩合并
当union的节点都是线性的,那么从0,1000都是一条直线,那么当查找0的根节点时,他需要遍历1000次,效率比较低
现在设置新的数组秩,按秩进行一个合并,也就是按树高来进行合并,谁的树高,谁的树根就是新的树根,当两个树根相等的时候,随便选一个,但是同时将新的根的那棵树的树高加1
2.路径压缩
public int find(int x){
if(parent[x] == x)return x;
while(parent[x] != x){
//路径压缩优化
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
优化后的判断树是否有环
public class DisjoinSet {
static final int N = 6;
public int find(int x, int[] parent, int[] rank){
if(parent[x] != -1){
x = find(parent[x], parent, rank);
}
return x;
//return x == parent[x] ? x : find(parent[x], paren)
}
public boolean union(int x, int y, int[] parent, int[] rank){
int x_root = find(x, parent, rank);
int y_root = find(y, parent, rank);
if(x_root == y_root)return false;
if(rank[x_root] < rank[y_root]){
parent[x_root] = y_root;
}else if(rank[x_root] > rank[y_root]){
parent[y_root] = x_root;
}else{
parent[x_root] = y_root;
rank[y_root]++;
}
return true;
}
@Test
public void test(){
int[] parent = new int[N];
int[] rank = new int[N];
Arrays.fill(rank, 1);
Arrays.fill(parent, -1);
int[][] edges = {{0, 1}, {1, 2}, {1, 3}, {2, 5}, {3, 4}}
for(int i = 0; i < N - 1; i++){
int x = edges[i][0];
int y = edges[i][1];
if(!union(x, y, parent, rank)) {
System.out.println("Cycle existed!");
}
}
System.out.println("UnCycle");
}
}
扁平化的策略有rank控制树高,和size获取树节点个数
4.LeetCode并查集
128. 最长连续序列
HashMap<Integer, Integer> find = new HashMap<>();
HashMap<Integer, Integer> rank = new HashMap<>();
int max = 1;
public int longestConsecutive(int[] nums) {
if(nums.length == 0 || nums == null)return 0;
for(int num : nums){
//初始化
find.put(num, num);
rank.put(num, 1);
}
for(int num : nums){
if(find.containsKey(num - 1)){
union(num, num - 1);
}
}
return max;
}
public int unionFind(int x){
int x_root = find.get(x);
if(x_root != x){
x_root = unionFind(x_root);
}
find.put(x, x_root);
return x_root;
}
public void union(int x, int y){
int x_root = unionFind(x);
int y_root = unionFind(y);
if(x_root == y_root)return;
int x_rank = rank.get(x_root);
int y_rank = rank.get(y_root);
if(x_rank > y_rank){
find.put(y_root, x_root);
rank.put(x_root, x_rank + y_rank);
}else{
find.put(x_root, y_root);
rank.put(y_root, x_rank + y_rank);
}
max = Math.max(max, x_rank + y_rank);
}
261. 以图判树
int[] parent;
int[] rank;
int count;
public boolean validTree(int n, int[][] edges) {
parent = new int[n];
rank = new int[n];
Arrays.fill(parent, -1);
Arrays.fill(rank, 1);
count = n;
for(int i = 0; i < edges.length; i++){
int x = edges[i][0];
int y = edges[i][1];
if(!union(x, y))return false;
}
return count == 1;
}
public int find(int x){
return parent[x] == -1 ? x : find(parent[x]);
}
public boolean union(int x, int y){
int x_root = find(x);
int y_root = find(y);
if(x_root == y_root)return false;
if(rank[x_root] > rank[y_root]){
parent[y_root] = x_root;
}else if(rank[y_root] > rank[x_root]){
parent[x_root] = y_root;
}else{
parent[x_root] = y_root;
rank[y_root]++;
}
count -= 1;
return true;
}
684. 冗余连接
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
UnionFind f = new UnionFind(n);
for(int i = 0; i < n; i++){
int x = edges[i][0];
int y = edges[i][1];
if(!f.union(x, y))return edges[i];
}
return new int[]{-1, -1};
}
class UnionFind{
int[] parent;
int[] rank;
public UnionFind(int n){
parent = new int[n + 1];
rank = new int[n + 1];
for(int i = 1; i < n + 1; i++){
parent[i] = i;
rank[i] = 1;
}
}
public boolean union(int x , int y){
int x_root = find(x);
int y_root = find(y);
if(x_root == y_root)return false;
if(rank[x_root] > rank[y_root]){
parent[y_root] = x_root;
}else if(rank[x_root] < rank[y_root]){
parent[x_root] = y_root;
}else{
parent[x_root] = y_root;
rank[y_root]++;
}
return true;
}
public int find(int x){
if(parent[x] == x)return x;
while(parent[x] != x){
//路径压缩优化
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
}
721. 账户合并
public List<List<String>> accountsMerge(List<List<String>> accounts) {
int n = accounts.size();
HashMap<String, Integer> emailToId = new HashMap<>();
UnionFind f = new UnionFind(n);
for(int i = 0; i < n; i++){
List<String> list = accounts.get(i);
//先不管帐户名只记id
for(int j = 1; j < list.size(); j++){
String email = list.get(j);
if(!emailToId.containsKey(email)){
emailToId.put(email, i);
}else{
//如果map里有了地址,那么肯定有同名账户,则进行合并
f.union(i, emailToId.get(email));
}
}
}
//记录根节点id对应的email
HashMap<Integer, List<String>> idToEmail = new HashMap<>();
for(String email : emailToId.keySet()){
int id = emailToId.get(email);
//找到根节点id
int root = f.find(id);
idToEmail.computeIfAbsent(root, z -> new ArrayList<>()).add(email);
}
//根据id到账户中取出帐户名,对结果进行处理
List<List<String>> res = new ArrayList<>();
for(int id : idToEmail.keySet()){
String name = accounts.get(id).get(0);
List<String> list = idToEmail.get(id);
Collections.sort(list);
list.add(0, name);
res.add(list);
}
return res;
}
class UnionFind{
int[] parent;
int[] rank;
public UnionFind(int n){
parent = new int[n];
rank = new int[n];
Arrays.fill(parent, -1);
Arrays.fill(rank, 1);
}
public int find(int x){
return parent[x] == -1 ? x : find(parent[x]);
}
public void union(int x, int y){
int x_root = find(x);
int y_root = find(y);
if(x_root == y_root)return;
if(rank[x_root] > rank[y_root]){
parent[y_root] = x_root;
}else if(rank[y_root] > rank[x_root]){
parent[x_root] = y_root;
}else{
parent[x_root] = y_root;
rank[y_root]++;
}
}
}
765. 情侣牵手
public int minSwapsCouples(int[] row) {
int len = row.length;
int n = len / 2;
//索引对d
UnionFind f = new UnionFind(n);
for(int i = 0; i < len; i += 2){
//情侣对的索引在除以2时是一样的
f.union(row[i] / 2, row[i + 1] / 2);
}
return n - f.count;
}
class UnionFind{
int[] parent;
int count;
public UnionFind(int n){
parent = new int[n];
for(int i = 0; i < n; i++){
parent[i] = i;
}
count = n;
}
public void union(int x, int y){
int x_root = find(x);
int y_root = find(y);
if(x_root == y_root)return;
parent[x_root] = y_root;
count--;
}
public int find(int x){
if(x == parent[x])return x;
//路径压缩
while(x != parent[x]){
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
}
MST17.07. 婴儿名字
public String[] trulyMostPopular(String[] names, String[] synonyms) {
UnionFind f = new UnionFind();
for(String s : names){
int index = s.indexOf("(");
String name = s.substring(0, index);
int count = Integer.parseInt(s.substring(index + 1, s.length() - 1));
//初始化
f.parent.put(name, name);
f.num.put(name, count);
}
for(String s: synonyms){
int index = s.indexOf(",");
String name1 = s.substring(1, index);
String name2 = s.substring(index + 1, s.length() - 1);
if(!f.parent.containsKey(name1)){
f.parent.put(name1, name1);
f.num.put(name1, 0);
}
if(!f.parent.containsKey(name2)){
f.parent.put(name2, name2);
f.num.put(name2, 0);
}
//合并
f.union(name1, name2);
}
List<String> res = new ArrayList<>();
for(String key : names){
int index = key.indexOf("(");
String name = key.substring(0, index);
//这里要提前判断,如果在res里判断是否contains,则会超时
//因为find了一遍,在res里又需要找一遍,耗时
//只要当前name不是root则continue
if(!f.find(name).equals(name))continue;
res.add(name + "(" + f.num.get(name) + ")");
}
return res.toArray(new String[res.size()]);
}
class UnionFind{
HashMap<String, String> parent;
HashMap<String, Integer> num;
public UnionFind(){
parent = new HashMap<>();
num = new HashMap<>();
}
public void union(String x, String y){
String x_root = find(x);
String y_root = find(y);
if(x_root.equals(y_root))return;
if(x_root.compareTo(y_root) < 0){
parent.put(y_root, x_root);
num.put(x_root, num.get(x_root) + num.get(y_root));
}else{
parent.put(x_root, y_root);
num.put(y_root, num.get(x_root) + num.get(y_root));
}
}
public String find(String x){
// if(x.equals(parent.get(x)))return x;
// //路径压缩
// parent.put(x, find(parent.get(x)));
// return parent.get(x);
//测试效率是一样的
return x.equals(parent.get(x)) ? x : find(parent.get(x));
}
}
总结
在刷了几道题之后,并查集的套路就是熟悉了,建立数据结构,union和find,先初始化,根据题目给的条件进行union,最后如果有要输出结果集的则find的根进行相应的统计即可