文章目录
前言
什么是并查集?
并查集是用来处理不同集合的合并以及查询的结构,可以理解为一种树形的数据结构,主要分为以下俩个大功能:
合并(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;
}
}
在复杂度上可能要高一点不如离散化,但是暴力求解可以很方便去解决这一系列问题。(代码没有进行详细的优化,并非最优解)