并查集分析以及代码实现(包含利用并查集处理程序自动分析问题)


前言

什么是并查集?

并查集是用来处理不同集合的合并以及查询的结构,可以理解为一种树形的数据结构,主要分为以下俩个大功能:

合并(Union):把两个不相交的集合合并为一个集合。
查询(Find):查询两个元素是否在同一个集合中。

一、如何合并以及查询

1.初始化

a.元素初始化(创建父类节点)

在初始化并查集时,我们默认一种操作便是所有元素首先指向自己,而指向本身的操作也是同时创建父类节点,不难看出一开始所有的元素的父类节点都是该元素本身。而每个元素的父类节点只有一个。
图文如下:
每个元素指向自己
那么是所有元素都指向本身吗?
当然不是了,因为每个元素的父类节点只有一个,所以只有根节点的元素会指向自己。

b.创建存放元素的尺寸的集合

初始化时,每个元素都是一个集合,如果存在多个元素处于一个集合的话,就会存在集合尺寸的这一个属性。
一个集合
由上图可见,一个集合的尺寸为4。那么有人会问创建一个集合的尺寸有什么用呢?在这里个人认为,虽然并查集主要是一个查询、合并的一种结构,但是不能失去它本应该拥有的功能。如给你n个元素,你通过并查集查询、合并成新的集合后,这时你再想得到该集合的元素的个数,那需要去遍历整个集合。所以创建尺寸集合是很有必要的。

2.合并操作(Union)

在合并操作时,我们先需要查询他的根节点6是不是同一个节点,如果是同一节点,那么我们不需要去合并(因为他们已经是同一个集合了)。
在这里插入图片描述
由此可以 5 ,6 存在于同一个节点,并且他们的根节点都是 1,所以通过判断根节点是不是同一节点,便可以看出元素是不是在一个集合当中!

当俩个元素的根节点不一样的时候
在这里插入图片描述
我们可以根据尺寸大小来合并俩个集合,尺寸小的集合的根节点指向尺寸大的集合
在这里插入图片描述
这里 7 的父节点就指向了 1。
根据尺寸大小来判断父节点是谁,我认为是为了减少一定的时间复杂度。这里不过多进行介绍。

3.查询操作(Find)

查询操作是判断俩个集合是不是同一集合,但它的本质却是俩个集合的父节点是不是同一个节点!简单来说,就是一个找父节点的操作。在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
通过上图所示,当查到父节点就等于自己的时候,这时便是这个集合的根节点!!!这也是为什么初始化时,每个元素必须指向自己!

二、并查集的代码实现过程

1.常规实现过程

a.初始化实现

代码如下

	static int f [] = new int[1000];//index是元素自己,值是父节点
    static int size[] = new int [1000];//元素大小
    void init(int f[],int size[]){
        for (int i = 0; i < 1000; i++) {
            f[i] = i ;//每个节点都指向自己
            size[i] = 1;//每个元素大小初始化为1
        }
    }

b.查询实现

代码如下

    static int FindFather(int a ){
        if(a!=f[a]) f[a] = FindFather(f[a]);//将路径上所有的子节点都指向根节点
        return f[a];
    }

c.合并实现

代码如下

    static void Union(int a,int b ){
        a = FindFather(a);
        b = FindFather(b);
        if(a==b) return;
        /*  如果a的集合大,a的集合大小要加上b的集合大小
        *   并且b的父节点指向a
        */
        if(size[a]>=size[b]){
            size[a] +=size[b];
            f[b] = a;
        }else {
            size[b]+=size[a];
            f[a] =b;
        }

    }

d总代码

代码如下

    static int f [] = new int[1000];//index是元素自己,值是父节点
    static int size[] = new int [1000];//元素大小

    void init(int f[],int size[]){
        for (int i = 0; i < 1000; i++) {
            f[i] = i ;//每个节点都指向自己
            size[i] = 1;//每个元素大小初始化为1
        }
    }
    static int FindFather(int a ){
        if(a!=f[a]) f[a] = FindFather(f[a]);//将路径上所有的子节点都指向根节点
        return f[a];
    }

    static void Union(int a,int b ){
        a = FindFather(a);
        b = FindFather(b);
        if(a==b) return;
        /*  如果a的集合大,a的集合大小要加上b的集合大小
        *   并且b的父节点指向a
        */
        if(size[a]>=size[b]){
            size[a] +=size[b];
            f[b] = a;
        }else {
            size[b]+=size[a];
            f[a] =b;
        }

    }

2.封装实现

这个代码来源于左程云老师,并非原创。

public class UnionFind {
    //封装成一个对象!
    public static class Element<V>{
        public V value;
        public Element(V value){
            this.value=value;
        }
    }

    //并查集的建立,每个数的father节点指向自己
    public static class UnionFindSet<V>{
        HashMap<V,Element<V>>  ElementMap;//用哈希表建立一个元素表
        HashMap<Element<V>,Element<V>> fatherMap;//用哈希表建立一个父亲表
        HashMap<Element<V>,Integer> rankMap;//用哈希表建立一个尺寸表
        UnionFindSet(List<V> list){//遍历,读入数据
            ElementMap = new HashMap<>();
            fatherMap = new HashMap<>();
            rankMap = new HashMap<>();
            for (V value : list) {
                Element element = new Element<V>(value);//封装成元素
                ElementMap.put(value,element);//加入元素表
                fatherMap.put(element,element);//加入父亲表,所有元素首先指向自己
                rankMap.put(element,1);//尺寸表,初始值为1
            }
        }

        //找到该元素的father节点
        Element<V> FindHead(Element<V> element ){
            Stack<Element<V>> path = new Stack<>();//建立一个栈
            while(element!=fatherMap.get(element)){//当这个元素的父节点指向自己的时候停止
                path.push(element);//将路径上所有元素进栈
                element = fatherMap.get(element);
            }
            while(!path.isEmpty()){
                fatherMap.put(path.pop(),element);//将栈上元素全部指向父节点
            }
            return element;
        }

        //是否是同一个father节点
        public boolean isSameSet(V a,V b){
            if(ElementMap.containsKey(a)&&ElementMap.containsKey(b)){
                return FindHead(ElementMap.get(a))==FindHead(ElementMap.get(b));
            }
            return false;
        }
        //合并
        public void Union(V a,V b){
            if(ElementMap.containsKey(a)&&ElementMap.containsKey(b)){
                Element<V> a1 =  FindHead(ElementMap.get(a));
                Element<V> b1 = FindHead(ElementMap.get(b));
                if(a1!=b1){
                    Element<V> big = rankMap.get(a1)>rankMap.get(b1)? a1:b1;
                    Element<V> small= big==a1? b1:a1;
                    fatherMap.put(small,big);
                    rankMap.put(big,rankMap.get(big)+rankMap.get(small));
                    rankMap.remove(small);
                }

            }
        }

    }
}

三、利用并查集暴力破解程序自动分析

题目如下
例如,一个问题中的约束条件为:x1=x2,x2=x3,x3=x4,x1≠x4,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
现在给出一些约束满足问题,请分别对它们进行判定。
输入格式
输入文件的第 1 行包含 1 个正整数 t,表示需要判定的问题个数,注意这些问题之间是相互独立的。
对于每个问题,包含若干行:
第 1 行包含 1 个正整数 n,表示该问题中需要被满足的约束条件个数。
接下来 n 行,每行包括 3 个整数 i,j,e,描述 1 个相等/不等的约束条件,相邻整数之间用单个空格隔开。若 e=1,则该约束条件为 xi=xj;若 e=0,则该约束条件为 xi≠xj。
输出格式
输出文件包括 t 行。
输出文件的第 k 行输出一个字符串 YES 或者 NO,YES 表示输入中的第 k 个问题判定为可以被满足,NO 表示不可被满足。
数据范围
1≤n≤10^5
1≤i,j≤10^9
输入样例:
2
2
1 2 1
1 2 0
2
1 2 1
2 1 1
输出样例:
NO
YES

这题的数值非常大,如果常规写法的并查集去解决,那么要离散化。但是在封装的写法,我们可以去暴力并查集!!

1 查询操作

        static int findhead(int a){
            Stack<Integer> stack = new Stack<>(); 
            while(a!=fatherMap.get(a)){
                stack.push(a);
                a = fatherMap.get(a);
            }
            while(!stack.isEmpty()){
                fatherMap.put(stack.pop(),a);
            }
            return a;
        }

2.合并操作

                if(!element.contains(i) && !element.contains(j)) {
                 //如果elemnt集合没有i,j俩个元素
                    element.add(i);//添加i元素
                    element.add(j);//添加j元素
                    if (e == 1) {
                        fatherMap.put(i, j);
                        fatherMap.put(j, j);
                    } else {
                        fatherMap.put(i, i);
                        fatherMap.put(j, j);
                    }
                } else if (element.contains(i) && element.contains(j)) {
                //如果element存在俩个元素
                    if (e == 1&&findhead(i)!=findhead(j))//如果俩个父亲节点是不一样的,需要合并	
                        fatherMap.put(findhead(i), findhead(j));
                } else {
                //有一个元素存在element中
                    if (element.contains(i)) {
                        element.add(j);
                        if (e == 1) {
                            fatherMap.put(j, i);
                        } else {
                            fatherMap.put(j, j);
                        }
                    } else if (element.contains(j)) {
                        element.add(i);
                        if (e == 1) {
                            fatherMap.put(i, j);
                        } else {
                            fatherMap.put(i, i);
                        }
                    }

3.总代码

import java.util.*;

class Main{
        static Scanner sc = new Scanner(System.in);
        static HashSet<Integer> element;
        static HashMap<Integer,Integer> fatherMap;
        public static void main(String args[]){
            int n = sc.nextInt();
            while(n>0){
                if(T()) System.out.println("YES");
                else System.out.println("NO");
                n--;
            }
        }
        static int findhead(int a){
            Stack<Integer> stack = new Stack<>(); 
            while(a!=fatherMap.get(a)){
                stack.push(a);
                a = fatherMap.get(a);
            }
            while(!stack.isEmpty()){
                fatherMap.put(stack.pop(),a);
            }
            return a;
        }
        static boolean T() {
            fatherMap = new HashMap<>();
            element = new HashSet<>();
            int N = sc.nextInt();
            int n = N;
            int fi[] = new int[N];
            int fj[] = new int[N];
            int fe[] = new int[N];
            while (N > 0) {
                int i = sc.nextInt();
                int j = sc.nextInt();
                int e = sc.nextInt();
                fi[N-1] = i;
                fj[N-1] = j;
                fe[N-1] = e;
                if(!element.contains(i) && !element.contains(j)) {
                    element.add(i);
                    element.add(j);
                    if (e == 1) {
                        fatherMap.put(i, j);
                        fatherMap.put(j, j);
                    } else {
                        fatherMap.put(i, i);
                        fatherMap.put(j, j);
                    }
                } else if (element.contains(i) && element.contains(j)) {
                    if (e == 1&&findhead(i)!=findhead(j))
                        fatherMap.put(findhead(i), findhead(j));
                } else {
                    if (element.contains(i)) {
                        element.add(j);
                        if (e == 1) {
                            fatherMap.put(j, i);
                        } else {
                            fatherMap.put(j, j);
                        }
                    } else if (element.contains(j)) {
                        element.add(i);
                        if (e == 1) {
                            fatherMap.put(i, j);
                        } else {
                            fatherMap.put(i, i);
                        }
                    }
                }
                N--;
            }
            while(n>0){
                if(fe[n-1]==1){
                    if(findhead(fi[n-1])!=findhead(fj[n-1])) return false;
                }else{
                    if(findhead(fi[n-1])==findhead(fj[n-1])) return false;
                }
                n--;
            }
            return true;
        }
}

在复杂度上可能要高一点不如离散化,但是暴力求解可以很方便去解决这一系列问题。(代码没有进行详细的优化,并非最优解)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值