目前已更新系列
当前--并查集系列
模板和使用
使用场景
- 一开始每个元素都拥有自己的集合,在自己的集合里只有自己这个元素
- int find(a):查看a所在集合的代表元素,代表元素来代表a所在集合
- bool isSameSet(a,b):判断a,b是否在同一集合中
- void union(a,b):合并a所属的集合和b所属的集合 :小的集合挂到大的集合
- 给中操作次数调用的均摊时间为O(1)
并查集优化
- 扁平化(一定要做)
- 小挂大
模板
import java.util.Scanner;
/**
*@ClassName 递归版本
*@Description TODO
*@Author Mr.Wang
*@Date 2024/3/2 20:36
*@Version 1.0
*/
public class 递归版本 {
static int MAXN=100001;
static int[] father=new int[MAXN];
static int n;
static void build(){
for (int i = 1; i <=n ; i++) {
father[i]=i;
}
}
public static int find(int i){
if (i!=father[i]){
father[i]=find(father[i]);
}
return father[i];
}
public static boolean isSameSet(int a,int b){
return find(a)==find(b);
}
public static void union(int a ,int b){
//不优化小挂大
// 同一将a的集合挂在b上
father[find(b)]=find(a);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n=scanner.nextInt();
int m=scanner.nextInt();
build();
for (int i = 0; i < m; i++) {
int opt=scanner.nextInt();
int a=scanner.nextInt();
int b= scanner.nextInt();
if (opt==1) union(a,b);
else System.out.println(isSameSet(a,b)==true?"Y":"N");
}
}
}
情侣牵手
题目描述
//思路:求交换最少次数,其实说到底就是将总的情侣对数-坐在一起的情侣对数(即没坐在一起的情侣对数有多少就最少交换多少次,这样来说其实不用并查集也可以做
//使用并查集只是类似于模版书写
//难点就是将人的号数映射为对应的情侣编号比如0,1映射为0号情侣
class Solution {
//并查集模版书写
private int[] father;
private int set;
private void build(int n){
father=new int[n];
for (int i = 0; i < n; i++) {
father[i]=i;
}
set=n;
}
//
private int find(int x){
if (father[x]!=x){
father[x]=find(father[x]);
}
return father[x];
}
private void union(int x,int y){
int fx=find(x);
int fy=find(y);
if (fy==fx){
father[fy]=fx;
set--;
}
}
public int minSwapsCouples(int[] row) {
int n=row.length;
build(n/2);
for (int i = 0; i < n-1; i++) {
union(row[i]/2,row[i+1]/2);
}
return n-set;
}
}
相似字符串
题目描述
解析
首先明白题目含义:
题目需要找数组中有几个相似集合,相似定义就是两个串,只有有个字符位置不同就是相似,然后一个相似集合中含有一个相似串即可,即集合中的每一个串和相似集合中的任意一个串相似就可以放入该集合,
了解题意后,这个很适合用并查集,遍历将相似的使用并查集放入一个集合中就好了
由于要找到所有集合,那么就不可避免的需要对比每两个字符串之间是否相似,所以这里是O(n2),然后比对相似需要一个O(n)
class Solution {
//思路:
//准备一个factor数组,factor[i]=x,表示i这个质因子已经被下标x元素占有了
//遍历数组元素,num[i]=a,我们先去找a的值因子分解,然后看看这些质数因子是否已经被占有
//如果之前被占有那么并查集合并两个元素集合
//如果没有被占有说明factor[i]=-1,那么说明i因子是第一次出现,标记factor[s]=i
private int MAXN=20001;
private int MAXV=100010;
private int[] father=new int[MAXN];
//factor[i]=x:表示i这个质因子代表第一个拥有i这个质数因子的元素是下标i
private int[] factor=new int[MAXV];
//用来存放集合中的元素个数size[i]=x:表示以值因子关联为统一集合的元素个数为x个
private int[] size=new int[MAXN];
private int n=0;
//并查集模板
private void build(){
for (int i = 0; i < n; i++) {
father[i]=i;
//一开始每个数都是一个独立集合,每个集合中有一个元素
size[i]=1;
}
Arrays.fill(factor,-1);
// Arrays.fill(size,0,n,1);
}
private int find(int x){
if (father[x]!=x){
father[x]=find(father[x]);
}
return father[x];
}
private void union(int x,int y){
int fx=find(x);
int fy=find(y);
if (fx!=fy){
father[fy]=fx;
//将y的个数累加到x上
size[fx]+=size[fy];
}
}
public int maxSize(){
int ans=0;
for (int i = 0; i <n; i++) {
ans=Math.max(ans,size[i]);
}
return ans;
}
public int largestComponentSize(int[] nums) {
n=nums.length;
build();
for (int i = 0; i < n; i++) {
int x=nums[i];
//质数因子分解
for (int j = 2; j*j <=x ; j++) {
if (x%j==0){
//j是x的质因子
if (factor[j]!=-1){
union(factor[j],i);
}else {
factor[j]=i;
}
while (x%j==0){
x/=j;
}
}
}
if (x>1){
//说明x分解来还剩最后x分解后的值因子
//j是x的质因子
if (factor[x]!=-1){
union(factor[x],i);
}else {
factor[x]=i;
}
}
}
return maxSize();
}
}
按公因数计算最大组件大小
题目描述
解析
看题目就是要返回数组中连通后,返回总的集合数量,一看就有一股并查集的味道,并查集做的不就是这个事情吗,这个不就是并查集所做的事情嘛,所以我们现在主要任务就是两个集合什么时候合并,从题目描述来看,两个集合合并的要求是当前数字和集合中的某个数字具有公共因子,我们不可能当前遍历到i时候还要去一个一个对比之前的数然后找到两个数的公共因子,然后合并i,j这两个数,虽然能得到结果,但是时间复杂度就上升一个维度了,所以想到用空间换时间,我们既然是通过公共质因子连接集合的,那么我们直接开辟一个数组factor[i]=x,比怕是i这个质因子第一个拥有他的是数组总的x号元素,然后遍历后面元素时分解元素质因子发现当这个质因子已经被占有了,那么我们就将这个占有元素取出来,和当前元素进行并查集
由于要返回的是集合中最大的个数,那么我们就需要再开一个数组,用来表示每个集合中的元素个数,最后返回最大
class Solution {
//思路:
//准备一个factor数组,factor[i]=x,表示i这个质因子已经被下标x元素占有了
//遍历数组元素,num[i]=a,我们先去找a的值因子分解,然后看看这些质数因子是否已经被占有
//如果之前被占有那么并查集合并两个元素集合
//如果没有被占有说明factor[i]=-1,那么说明i因子是第一次出现,标记factor[s]=i
private int MAXN=20001;
private int MAXV=100010;
private int[] father=new int[MAXN];
//factor[i]=x:表示i这个质因子代表第一个拥有i这个质数因子的元素是下标i
private int[] factor=new int[MAXV];
//用来存放集合中的元素个数size[i]=x:表示以值因子关联为统一集合的元素个数为x个
private int[] size=new int[MAXN];
private int n=0;
//并查集模板
private void build(){
for (int i = 0; i < n; i++) {
father[i]=i;
//一开始每个数都是一个独立集合,每个集合中有一个元素
size[i]=1;
}
Arrays.fill(factor,-1);
// Arrays.fill(size,0,n,1);
}
private int find(int x){
if (father[x]!=x){
father[x]=find(father[x]);
}
return father[x];
}
private void union(int x,int y){
int fx=find(x);
int fy=find(y);
if (fx!=fy){
father[fy]=fx;
//将y的个数累加到x上
size[fx]+=size[fy];
}
}
public int maxSize(){
int ans=0;
for (int i = 0; i <n; i++) {
ans=Math.max(ans,size[i]);
}
return ans;
}
public int largestComponentSize(int[] nums) {
n=nums.length;
build();
for (int i = 0; i < n; i++) {
int x=nums[i];
//质数因子分解
for (int j = 2; j*j <=x ; j++) {
if (x%j==0){
//j是x的质因子
if (factor[j]!=-1){
union(factor[j],i);
}else {
factor[j]=i;
}
while (x%j==0){
x/=j;
}
}
}
if (x>1){
//说明x分解来还剩最后x分解后的值因子
//j是x的质因子
if (factor[x]!=-1){
union(factor[x],i);
}else {
factor[x]=i;
}
}
}
return maxSize();
}
}
小美的关系
题目描述
4.
小美的朋友关系
小美认为,在人际交往中,但是随着时间的流逝,朋友的关系也是会慢慢变淡的,最终朋友关系就淡忘了。
现在初始有一些朋友关系,存在一些事件会导致两个人淡忘了他们的朋友关系。小美想知道某一时刻中,某两人是否可以通过朋友介绍互相认识?
事件共有 2 种:
1 u v:代表编号 u 的人和编号 v 的人淡忘了他们的朋友关系。
2 u v:代表小美查询编号 u 的人和编号 v 的人是否能通过朋友介绍互相认识。
注:介绍可以有多层,比如 2 号把 1 号介绍给 3 号,然后 3 号再把 1 号介绍给 4 号,这样 1 号和 4 号就认识了。
时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 256M,其他语言512M
输入描述:
第一行输入三个正整数,代表总人数,初始的朋友关系数量,发生的事件数量。
接下来的行,每行输入两个正整数,代表初始编号的人和编号的人是朋友关系。
接下来的行,每行输入三个正整数,含义如题目描述所述。
保证至少存在一次查询操作。
输出描述:
对于每次 2 号操作,输出一行字符串代表查询的答案。如果编号 u 的人和编号 v 的人能通过朋友介绍互相认识,则输出"Yes"。否则输出"No"。
示例1
输入例子:
5 3 5
1 2
2 3
4 5
1 1 5
2 1 3
2 1 4
1 1 2
2 1 3
输出例子:
Yes
No
No
例子说明:
第一次事件,1 号和 5 号本来就不是朋友,所以无事发生。
第二次事件是询问,1 号和 3 号可以通过 2 号的介绍认识。
第三次事件是询问,显然 1 号和 4 号无法互相认识。
第四次事件,1 号和 2 号淡忘了。
第五次事件,此时 1 号无法再经过 2 号和 3
解析
这题是一开始直接做没做出来,看到题目的时候感觉这个题目很适合并查集,但是继续看题目的时候,发现对于并查集来说查询是比较容易的,但题目需要支持删除一条边操作,而并查集删除边之后对于集合的划分不好进行比如下面要删除23这条边,原本代表都是1,现在删除23之后,本来12是一个集合,34是一个集合,但是我们并查集中并不好操作,所以当时就卡住了
后面借鉴了其他题友的解决方法有几个注意点
- 1、对于题目n是10^9次方,如果按照正常的开一个father数组是用不了的,所以需要进行离散化(将无限的空间映射到有限的空间)但是也可以不映射,我们直接使用map这种集合里面具有自动扩容机制
- 2、对于之前卡住的点,建立好并查集后并不好删除边,所以这里采用逆向思维
-
- 我们题目相当于给出基本的边然后输入op操作,如果是1表示删除关系,那么既然我们不好对并查集进行删除操作,那么反过来我们给他进行添加操作
- 怎么添加尼?因为如果按照op的顺序执行那么后面的操作确实是依赖于前面的操作的,但是如果我们第一遍现将op操作存起来,然后记录下需要删除的边,然后我们反过来遍历,即先处理后面的操作再处理前面的操作,这样,后面的是不依赖于前面的,然后遍历op操作是如果是删除操作那么就对并查集进行加边,即合并集合操作
- 具体就是
-
- 1、接收输入存放最开始的边eages
- 2、接收op操作,存放所有的op操作到op数组中,然后如果,如果是删除操作,那么记录是删除那条边,有一个前提是删除的这条边需要在eages中,因为如果没有这条边,那么就不存在删除这个关系的操作
- 3、开始建立并查集,遍历所有边,每条边要求不在删除边的集合中出现,这样相当于就是得到了删除所有要删除的边之后的eages了,建立的并查集就是对应删除所有边之后的集合了
- 4、最后我们逆向遍历刚才的存放所有op操作的集合,然后如果是查询那么直接用并查集查询,如果遇到了删除的边那么久将这条边上的两个点进行并查集合并操作
- 注意:
-
- 由于需要快速判断某条边是否在集合中,我们对于存放边的集合使用set来存放,然后就能快速判断每一个边key
- 对于这个代码是过不了所有用力的还是会超时,但是使用同样的算法给到C++,C++能够ac,我这里尝试优化java代码还是不能ac
package 秋招实习真题.美团.第一场.第四题.题解;
import java.io.*;
import java.util.*;
public class Main {
//先吧所有边存放起来
//将所有操作存放起来
//将所有删除的操作边存放起来(这个边要原来有)
//保存删除后的所有边
//反向遍历操作
//开始反向遍历剩余的边
static Map<Integer, Integer> father = new HashMap<>();
//并查集模板
public static void build(int n) {
//初始化
for (int i = 1; i <= n; i++) {
father.put(i, i);
}
}
public static int find(int x) {
if (father.get(x) != x) {
father.put(x, find(father.get(x)));
}
return father.get(x);
}
public static void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fy != fx) {
father.put(fx, fy);
}
}
public static boolean isSame(int x, int y) {
return find(x) == find(y);
}
public static void main(String[] args) throws IOException {
//怎么存边数组长度为2,0和1之间有一条边,注意不能用数组作为元素,因为后面需要判断边是否在集合里面,
//而数组是没有重写或者覆盖hashcode方法和equal方法的
// HashSet<int[]> eages = new HashSet<>();
HashSet<ArrayList<Integer>> eages = new HashSet<>();
//怎么寸操作:0:操作,1到2之间有一条边
ArrayList<int[]> opEage = new ArrayList<>();
//存放删除所有要删除边后剩余的边
HashSet<ArrayList<Integer>> delEage = new HashSet<>();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(System.out);
while (in.nextToken() != StreamTokenizer.TT_EOF) {
// in.nextToken();
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
in.nextToken();
int k = (int) in.nval;
for (int i = 0; i < m; i++) {
in.nextToken();
int u = (int) in.nval;
in.nextToken();
int v = (int) in.nval;
ArrayList<Integer> eage = new ArrayList<>();
eage.add(u);
eage.add(v);
eages.add(eage);
}
//存放操作
for (int i = 0; i < k; i++) {
in.nextToken();
int p = (int) in.nval;
in.nextToken();
int u = (int) in.nval;
in.nextToken();
int v = (int) in.nval;
opEage.add(new int[] {p, u, v});
// opEage.add(new int[]{p,v,u});
//如果是删除边,并且现在的边中含有
ArrayList<Integer> eage = new ArrayList<>();
eage.add(u);
eage.add(v);
if (p == 1 && eages.contains(eage)) {
delEage.add(eage);
//虽然图是无向图,但是这里没必要将两个方向都加入,因为我们删除的边的顺序和操作的顺序一致
// delEage.add(new int[]{v,u});
}
}
//开始建立并查集
build(n);
for (ArrayList<Integer> eage : eages) {
if (!delEage.contains(eage)) {
int u = eage.get(0);
int v = eage.get(1);
union(u, v);
}
}
//收集答案
ArrayList<String> ans = new ArrayList<>();
//逆向
for (int i = opEage.size() - 1; i >= 0; i--) {
int[] opeage = opEage.get(i);
int p = opeage[0];
int u = opeage[1];
int v = opeage[2];
if (p == 2) {
//查询
ans.add(isSame(u, v) ? "Yes" : "No");
} else {
//删除,如果在删除边中变成添加
ArrayList<Integer> eage = new ArrayList<>();
eage.add(u);
eage.add(v);
if (delEage.contains(eage)) {
union(u, v);
}
}
}
//结束后同样需要反向打印答案
for (int i = ans.size() - 1; i >= 0; i--) {
out.println(ans.get(i));
}
}
out.flush();
out.close();;
br.close();
}
}