在我个人理解,并查集是用于解决这样的问题:题目中的数据分多个集合,并且有合并的可能,有时需要查找两个元素是不是在同一集合,以及该集合中所有元素的数量。有时也需要通过两个元素在同一集合中的位置,来确定这两个元素之间的关系,
:这种并查集只需要两个功能:合并和查找祖宗结点。
需要的基本数据结构:p[N],用来存储每一个下标的父亲结点。
查找函数
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
这段函数用来返回元素x的祖宗结点,并且在函数递归的过程中,完成了路径压缩,即让所有的结点都指向祖宗结点,这样再次查找时递归次数就会减少,于是减少了时间复杂度,提高了效率。
合并函数
void myMerge(int a,int b)
{
p[find(a)]=find(b);
}
非常好理解,并查集里的每一个集合相当于一个树,将两个并查集合并,本质上就是让其中一个并查集的根节点认另一个并查集的根节点做父亲。
初始化
void init()
{
for(int i=1;i<=n;i++)
p[i]=i;
}
每一个元素的父亲结点初始时都是自己,即,如果这个结点是树的根节点,那么它就是自己的父亲结点。
这里我们可以理解为,每一个元素自己在初始时都是一个并查集,都是一棵棵树,只不过这些树都只有一个根节点,随着之后的合并操作,一些结点就不是根节点了,树的数量减少。
并查集初始化的下标一般从1开始。
这种并查集相比于朴素并查集,增加了一个操作:保存每一个并查集中的元素的个数,也就是size。
需要的数据结构:p[N],sz[N](用来存储每一个并查集的元素个数)
注意,只有祖宗结点的sz[]有意义,代表该并查集的元素个数。
查找函数:和朴素并查集的相同。
初始化函数
void init()
{
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
sz[i] = 1;
}
}
除了要初始化p数组外,还要初始化sz数组,每一个并查集初始时的元素数量都是1。
合并函数
void myMerge(int a,int b)
{
sz[find(b)]+=sz[find(a)];
p[find(a)]=find(b);
}
注意一定要先加sz再加p,因为p的改变会影响sz。也可以定义两个临时变量来存储find(a)和find(b),这样什么顺序都可以了。
有一些问题需要我们通过计算当前结点到祖宗结点的距离。而且有时还需要对这些距离取模,通过取模后的值,来表示当前结点与根节点的关系。
需要的数据结构:p[N],d[N],d[N]用来存储结点x到p[x]的距离。
注意:这个d数组表示的是当前结点到父亲结点的距离,不是到根节点的距离。尽管经过路径压缩后,d数组在逻辑上已经表示成了当前结点到根节点的距离,但定义上,d数组表示的还是当前结点到父亲结点的距离。
查找函数
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
在查找的过程中,一边路径压缩,一边更新d数组。注意,距离的更新一定要在p更新之前。
初始化函数
void init()
{
for(int i=1;i<=n;i++)
{
p[i]=i;
d[i]=0;
}
}
初始化时,每一个结点都是根结点,根结点到根结点的距离为0。
合并函数
void myMerge(int a,int b)
{
p[find(a)]=find(b);
d[find(a)]=distance;
}
这个distance的公式根据具体问题具体分析。
ACWING836 合并集合
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。
输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
AC代码:
#include
using namespace std;
const int N=100010;
int p[N],n,m;
void init()
{
for(int i=1;i<=n;i++)
p[i]=i;
}
int myFind(int x)
{
if(p[x]!=x)p[x]=myFind(p[x]);
return p[x];
}
void myMerge(int a,int b)
{
p[myFind(a)]=myFind(b);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int a,b;
char c;
cin>>n>>m;
init();
while(m--)
{
cin>>c>>a>>b;
if(c=='M')
myMerge(a,b);
if(c=='Q')
{
if(myFind(a)==myFind(b))
{
cout<<"Yes"<<'\n';
}
else
cout<<"No"<<'\n';
}
}
return 0;
}
ACWING 837 连通块中点的数量
给定一个包含n个点(编号为1~n)的无向图,初始时图中没有边。
现在要进行m个操作,操作共有三种:
“C a b”,在点a和点b之间连一条边,a和b可能相等;
“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
“Q2 a”,询问点a所在连通块中点的数量;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。
输出格式
对于每个询问指令”Q1 a b”,如果a和b在同一个连通块中,则输出“Yes”,否则输出“No”。
对于每个询问指令“Q2 a”,输出一个整数表示点a所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤10^5
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
AC代码:
#include
using namespace std;
const int N=100010;
int p[N],sz[N],n,m;
void init()
{
for(int i=1;i<=n;i++)
{
p[i]=i;
sz[i]=1;
}
}
int myfind(int x)
{
if(p[x]!=x)p[x]=myfind(p[x]);
return p[x];
}
void mymerge(int a,int b)
{
if(myfind(a)==myfind(b))return;
sz[myfind(b)]+=sz[myfind(a)];
p[myfind(a)]=myfind(b);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int a,b;
string s;
cin>>n>>m;
init();
while(m--)
{
cin>>s;
if(s=="C")
{
cin>>a>>b;
mymerge(a,b);
}
if(s=="Q1")
{
cin>>a>>b;
if(myfind(a)==myfind(b))cout<<"Yes"<<'\n';
else cout<<"No"<<'\n';
}
if(s=="Q2")
{
cin>>a;
cout<<sz[myfind(a)]<<'\n';
}
}
return 0;
}
ACWING240 食物链
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。
A 吃 B,B 吃 C,C 吃 A。
现有 N 个动物,以 1∼N 编号。
每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示 X 和 Y 是同类。
第二种说法是 2 X Y,表示 X 吃 Y。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
当前的话与前面的某些真的话冲突,就是假话;
当前的话中 X 或 Y 比 N 大,就是假话;
当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
输入格式
第一行是两个整数 N 和 K,以一个空格分隔。
以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。
若 D=1,则表示 X 和 Y 是同类。
若 D=2,则表示 X 吃 Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1≤N≤50000,
0≤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
AC代码:
#include
using namespace std;
const int N = 100010;
int p[N], d[N], res, n, k;
int myFind(int x)
{
if(p[x] != x)
{
int u = myFind(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int v, x, y, rx, ry;
cin >> n >> k;
for(int i = 1; i <= n; i++)
p[i] = i;
while(k--)
{
cin >> v >> x >> y;
if(x > n || y > n)
res++;
else
{
rx = myFind(x), ry = myFind(y);
if(v == 1)
{
if(rx == ry && (d[x] - d[y]) % 3)
res++;
else if(rx != ry)
{
p[rx] = ry;
d[rx] = d[y] - d[x];
}
}
else
{
if(rx == ry && (d[x] - d[y] - 1) % 3)
res++;
else if(rx != ry)
{
p[rx] = ry;
d[rx] = d[y] - d[x] + 1;
}
}
}
}
cout << res << '\n';
return 0;
}
思想:
这道题主要通过并查集当前结点x到根结点的距离来表示x和根节点的关系。
x到根节点的距离mod3=0:x和根节点是同类
x到根节点的距离mod3=1:x吃根节点
x到根节点的距离mod3=2:x被根节点吃
只有这三种情况,所以除了前两种情况外,else就是第三种。
合并结点
如果x和y是同类,则把x的根节点rx合并到y所在的并查集,符合公式:
(d[rx]+d[x]-d[y])%3=0
推导出
d[rx]=d[y]-d[x]+3k,k=1,2,...
由于最后判断时都要mod3,所以3k可以舍去,简化为
d[rx]=d[y]-d[x]
同理,如果x和y是x可以吃y的关系,则公式为:
(d[rx]+d[x]-d[y]-1)%3=0
也就是
d[rx]=d[y]-d[x]+1
还有就是if else判断时要注意,如果x和y的根节点不在一个并查集,就代表它俩的关系还不确定,这时我们无法判断它是真是假,只能采取“疑罪从无”原则,把它当成真,并合并x和y的根节点。