并查集及经典题
AcWing 836. 合并集合---》最简单的并查集操作:
一共有 nn 个数,编号是 1∼n1∼n,最开始每个数各自在一个集合中。
现在要进行 mm 个操作,操作共有两种:
M a b
,将编号为 aa 和 bb 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 aa 和 bb 的两个数是否在同一个集合中;
输入格式
第一行输入整数 nn 和 mm。
接下来 mm 行,每行包含一个操作指令,指令为 M a b
或 Q a b
中的一种。
输出格式
对于每个询问指令 Q a b
,都要输出一个结果,如果 aa 和 bb 在同一集合内,则输出 Yes
,否则输出 No
。
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
分析:如果不使用并查集,bf做法是维护一个数组a[],a[k]表示第k个数所在的集合,这样的话查询集合很方便,但是如果想合并集合的话会特别麻烦,引入并查集之后会很简便。
并查集算法的核心是维护一个很多个树,每个树都代表一个集合。
如果想找到某个元素属于哪个集合,就找到这个元素的父亲结点,如果父亲结点不是集合的祖宗结点就重复上面的操作直到找到祖宗结点即可,这样的话就可以从祖宗结点获取最开始的元素所述的集合是哪个集合(核心就是这些集合树,在维护树的过程中还可以保存一些额外的信息,比如当前集合有多少个元素等等)。为了找到父亲结点,因此对数中的每个元素我们都保存其父亲结点所在的下标。
如果想合并两个集合a,b,那就把b集合的祖宗结点的父亲结点设为a元素的任意结点(但是都是设为a的祖宗结点,为了减小数的层数,减小搜索时间)即可。
import java.io.*;
/*
本题最好是看视频,
注意:1.因为只需要找到某个数所在的集合,而不需要找某个集合有哪些数,因此是从子节点往祖宗结点找,因此存放的是parent,trie字典树放的是son!!
2.树越短,找父亲越快,因此在find函数里面还包含了路径压缩,非常精彩(在某些样例中如果不进行路径压缩,时间可能会差三倍左右)
*/
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
String[] split = reader.readLine().split(" ");
int n = Integer.parseInt(split[0]);
int m = Integer.parseInt(split[1]);
int[] parent = new int[n + 10];
for (int i = 0; i < n + 10; i++) {
parent[i] = i;
}
for (int i = 0; i < m; i++) {
String[] s = reader.readLine().split(" ");
if ("M".equals(s[0])) {
int a = Integer.parseInt(s[1]);
int parentOfa = findParent(a, parent);
int b = Integer.parseInt(s[2]);
int parentOfb = findParent(b, parent);
parent[parentOfb] = parentOfa;//合并:b的祖宗结点的值给改掉
} else if ("Q".equals(s[0])) {
int a = Integer.parseInt(s[1]);
int b = Integer.parseInt(s[2]);
if (findParent(a, parent) == findParent(b, parent)) {
System.out.println("Yes");
} else {
System.out.println("No");
}
} else {
System.out.println("input error~~~~");
}
}
}
private static int findParent(int num, int[] parent) {//找到祖宗结点+路径压缩
if (num != parent[num]) {
//以两个数合并为例(-》表示其祖宗节点),1——》1,2-》2,合并之后1-》2,2-》2,此时如果传入的是num=1,
// 那么还是会递归一次findParent(parent[num], parent)传入2,就不会递归了,因此路径压缩后最多递归一次
parent[num] = findParent(parent[num], parent);
}
return parent[num];
}
}
AcWing 837. 连通块中点的数量
给定一个包含 nn 个点(编号为 1∼n1∼n)的无向图,初始时图中没有边。
现在要进行 mm 个操作,操作共有三种:
C a b
,在点 aa 和点 bb 之间连一条边,aa 和 bb 可能相等;Q1 a b
,询问点 aa 和点 bb 是否在同一个连通块中,aa 和 bb 可能相等;Q2 a
,询问点 aa 所在连通块中点的数量;
输入格式
第一行输入整数 nn 和 mm。
接下来 mm 行,每行包含一个操作指令,指令为 C a b
,Q1 a b
或 Q2 a
中的一种。
输出格式
对于每个询问指令 Q1 a b
,如果 aa 和 bb 在同一个连通块中,则输出 Yes
,否则输出 No
。
对于每个询问指令 Q2 a
,输出一个整数表示点 aa 所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤1051≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
分析:关键是看出本题的连同两个点等价于并查集中合并两个集合,而找到某个点 aa 所在连通块中点的数量,其实就是在建立并查集的过程中维护一些额外的点数信息,又第一题的find函数我们可以很轻松的找到某个元素的祖宗(root)结点,因此我们只需要把额外的点数信息维护在祖宗结点下标对应的size[]中即可。
import java.io.*;
/*
本题只需要从儿子找父亲,因为维护fa集合即可
不用在意下标,爱用哪个用哪个就可以了!
*/
public class Main {
static int[] size = new int[100000 + 10];
static int[] fa = new int[100000 + 10];
public static void main(String[] args) throws IOException {
for (int i = 0; i < size.length; i++) {
size[i]=1;
fa[i]=i;
}
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
String[] split = reader.readLine().split(" ");
int n=Integer.parseInt(split[0]);
int m=Integer.parseInt(split[1]);
for (int i = 0; i < m; i++) {
String[] s = reader.readLine().split(" ");
if ("C".equals(s[0])) {
int a=Integer.parseInt(s[1]);
int b=Integer.parseInt(s[2]);
//如果原来没有连起来(即原来不在一个集合中)
if (findFa(a)!=findFa(b)){
size[findFa(b)]+=size[findFa(a)];
fa[findFa(a)]=findFa(b);
}
}else if ("Q1".equals(s[0])){
int a=Integer.parseInt(s[1]);
int b=Integer.parseInt(s[2]);
if (findFa(a)==findFa(b)){
writer.write("Yes\n");
}else {
writer.write("No\n");
}
}else if ("Q2".equals(s[0])){
int a=Integer.parseInt(s[1]);
writer.write(size[findFa(a)]+"\n");
}else {
System.out.println("input error");
}
}
reader.close();
writer.flush();
writer.close();
}
public static int findFa(int i){
if (fa[i] != i){
fa[i]=findFa(fa[i]);
}
return fa[i];
}
}
AcWing 240. 食物链
动物王国中有三类动物 A,B,CA,B,C,这三类动物的食物链构成了有趣的环形。
AA 吃 BB,BB 吃 CC,CC 吃 AA。
现有 NN 个动物,以 1∼N1∼N 编号。
每个动物都是 A,B,CA,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 NN 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y
,表示 XX 和 YY 是同类。
第二种说法是 2 X Y
,表示 XX 吃 YY。
此人对 NN 个动物,用上述两种说法,一句接一句地说出 KK 句话,这 KK 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 XX 或 YY 比 NN 大,就是假话;
- 当前的话表示 XX 吃 XX,就是假话。
你的任务是根据给定的 NN 和 KK 句话,输出假话的总数。
输入格式
第一行是两个整数 NN 和 KK,以一个空格分隔。
以下 KK 行每行是三个正整数 D,X,YD,X,Y,两数之间用一个空格隔开,其中 DD 表示说法的种类。
若 D=1D=1,则表示 XX 和 YY 是同类。
若 D=2D=2,则表示 XX 吃 YY。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤500001≤N≤50000,
0≤K≤1000000≤K≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3
分析:分析在代码中。。
import java.io.*;
/*
创造一个数组维护他们之间的种族关系
animal[0]不管,说是哪个动物就用哪个下标
疑点:怎么在合并动物时更新他们的种族信息(谁吃谁的信息):靠(路径%3)
*/
public class Main {
static int[] fa = new int[50000 + 10];
static int[] dis = new int[50000 + 10];
public static void main(String[] args) throws IOException {
for (int i = 0; i < dis.length; i++) {
fa[i] = i;
}
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
String[] split = reader.readLine().split(" ");
int n = Integer.parseInt(split[0]);
int k = Integer.parseInt(split[1]);
int res = 0;
for (int i = 0; i < k; i++) {
split = reader.readLine().split(" ");
int c = Integer.parseInt(split[0]);
int x = Integer.parseInt(split[1]);
int y = Integer.parseInt(split[2]);
if (x > n || y > n) {
res++;
continue;
}
int xf = findFa(x);
int yf = findFa(y);
if (c == 1) {
if (xf == yf && (dis[x] - dis[y]) % 3 != 0) {
res++;
} else if (xf != yf) {
//真话,合并xy所在的集合
//把yf当成了合并集合之后的祖宗结点
fa[xf] = yf;
//满足的关系是(xf到xy的距离)+(x到xf的距离)=y到yf的距离;
dis[xf] = dis[y] - dis[x];
}
} else if (c == 2) {
//(dis[a]-dis[b])%3==1表示可以a可以吃b
//自己吃自己在下面判断中可以判断出来,因此不进行特判了
if (xf == yf && (dis[x] - dis[y] - 1) % 3 != 0) {
//(dis[x] - dis[y]) % 3 != 1是错的,因为(a+b)%3!=a%3+b%3,可以带a,b为1,2
res++;
} else if (xf != yf) {
//真话,合并xy所在的集合
//把yf当成了合并集合之后的祖宗结点
fa[xf] = yf;
//满足的关系是(xf到xy的距离)+(x到xf的距离)=y到yf的距离+1;
dis[xf] = 1 + dis[y] - dis[x];
}
} else {
System.out.println("input error!");
}
}
writer.write(res + "");
reader.close();
writer.flush();
writer.close();
}
/*
find函数里面由于自带路径压缩,因此路径压缩时候必须更新dis数组
*/
public static int findFa(int x) {
if (fa[x] != x) {
//因为fa[x]的值会改变,因此先更新dis数组里面的值再更新fa[x]
// dis[x] += dis[fa[x]]; 注意这种写法是错的,因为findFa函数中路径压缩的思路是:如果此结点不是祖宗结点,那么就开始递归
// fa[x] = findFa(fa[x]); 递归到最后的结点就是祖宗结点,出递归后更新的逻辑是儿子的儿子直接指向祖宗,儿子的儿子的儿子指向祖宗
// 是从上往下更新的,因此在这个过程中的dis数组的更新也必须遵从这个逻辑:从辈分高的开始更新,或者说是
// 出递归之后以递归更新后的最新父辈的dis数组来更新子辈的dis数组,因为父辈的dis数组的变化会影响子
// 辈,而子辈的dis数组的变化不会影响父辈;
// 如果直接那么写逻辑就变成了:先更新子辈的dis,然后更新父辈的,此时会造成父辈dis的变化不会传递给子辈,
// 自然就错误了
//
int t=findFa(fa[x]); //因此这种写法是对的,先出递归,再更新dis数组!!!! y总强啊
dis[x]+=dis[fa[x]];
fa[x]=t;
}
return fa[x];
}
}