第一次听老师讲并查集还以为是很复杂的数据结构,实操之后发现用数组就可以模拟。
先是并查集的模板题。
P3367 【模板】并查集 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题解 P3367 【【模板】并查集】 - 加载错误 的博客 - 洛谷博客 (luogu.org)
这个大佬讲的很清楚。
#include
using namespace std;
const int MAX=1e4+10;
int f[MAX];
int parity[MAX];
void init(int n){
for(int i=0;i<=n;i++){
f[i]=i;
}//初始化掌门。掌门是他自己
}
int find_f(int x){
if(x!=f[x])//取值,压缩,更新
{
int t=f[x];
f[x]=find_f(x);
parity[x]^=parity[t];
}
//把这个子节点的老大换成老大的老大
return f[x];
}//压缩路径
void join(int x,int y){
int fx=find_f(x);
int fy=find_f(y);
if(fx!=fy)
{
f[fy]=fx;
}
}
int main(){
return 0;
}
简述一下并查集的几个部分:
1.初始化函数:init
每个人的掌门,也就是一开始所有人都没有联通的情况下,他们的掌门就是他们自己。用f[x]表示
2.寻根函数:find
寻找每个人的掌门,这个函数涉及到了路径压缩,即在查询掌门的时候,就把f数组的内容改成掌门,这样接下来的查询只需要一次就可以完成。
3.合并函数:join
寻找两个人的掌门,如果掌门不一样就选取一个掌门为根,合并起来。注意join之后每个f[x]存的内容都是自己的掌门,这得益于路径压缩。
并查集的应用:城市联通线路问题.
Problem - 1232 (hdu.edu.cn) HDU-1232
#include
#include
using namespace std;
const int MAX=1e3+10;
int f[MAX];
int count1[MAX];
void init(int n){
for(int i=1;i<=n;i++){
f[i]=i;
}//初始化掌门。掌门是他自己
}
int find_f(int x){
if(x!=f[x]) return f[x]=find_f(f[x]);//把这个子节点的老大换成老大的老大
return f[x];
}//压缩路径
void join(int x,int y){
int fx=find_f(x);
int fy=find_f(y);
if(fx!=fy) f[fy]=fx;
}//合并子集
int main(){
//首先用一个数组保存掌门
int n,t;
while(cin>>n&&n)
{
cin>>t;
init(n);
for(int i=0;i>a>>b;
join(a,b);
}
int ans=0;
for(int i=1;i<=n;i++)
{
if(f[i]==i)
ans++;
}
cout<
这个时候我踩了一个坑,问题在于我没有意识到join之后由于路径压缩fx指向的就是自己的掌门,在最后一次计算ans的时候用的是find函数遍历。导致时间增加。
最后有一个点,memset的赋值是赋给字节,所以最后一个参量最好是sizeof(arr)
总结一下,并查集给我的感觉就是适用于一个平面中有很多分散的点,然后给出了几个点的连接关系。最后查询这些点之间是否有联系。其实掌门就是随机选择了一个联通内的点,并没有什么特殊性,只不过方便寻找而已.
带权并查集:
P2024 [NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
带权并查集就是在点的链接关系之间多了一个权,请看题目。
#include
using namespace std;
const int MAX=5e4+10;
int f[MAX];
int chain[MAX]; //食物链关系,1是吃,0同类,2被吃
void init(int n){
for(int i=0;i<=n;i++){
f[i]=i;
}//初始化掌门。掌门是他自己
}
int find_f(int x){
if(x!=f[x])
{
int t=f[x];
f[x]=find_f(f[x]);
chain[x]=(chain[x]+chain[t])%3;
}
return f[x];
}
void join(int x,int y){
int fx=find_f(x);
int fy=find_f(y);
if(fx!=fy) f[fy]=fx;
}//合并子集
int main(){
int n,t;
cin>>n>>t;
init(n);
int count=0;
int pre=0;
for(int i=0;in||b>n)
{
count++;
}else if(a==b&&v==2)
{
count++;
}else
{
int fx=find_f(a);
int fy=find_f(b);
//这里可以缩到join里面
if(fx==fy)
{
if(v!=(chain[a]-chain[b]+3)%3)
count++;
}else
{
f[fx]=fy;//相当于把a接到了b上。
chain[fx]=(-chain[a]+v+chain[b])%3;
}
}
}
cout<
以下是做题的思路。
在他说的话没有明显错误时判断如下:
一开始我们并不知道两个动物之间有什么关系,也不知道他们是什么物种,但是当两个动物之间没有关系的时候(即两个物种所在的点不连通的时候),我们可以知道他说的话是真的。
根据他的话,不妨就把其中一个当成根节点,把value直接赋值给它。比如说a吃b那么a到b的关系可以直接赋值chainb为1,如果b吃c,chainc赋值为1,这样abc这条链上可以直接通过chain访问到a和c的关系。这样我们就给边附上了权
同理经过多次赋值之后,会出现两个生物都有了自己的根节点,就意味着有了自己的生态位,也说明了他们是联通的,可以通过边权来溯源寻找生物之间的关系。
因为这道题所有生物一定是abc的一种,所以生物间一定是联通的,只不过没有合并罢了,所以fx!=fy的时候说明现在他们没有形成判断体系,要根据他说的话合并。如果形成判断体系,然而他说的chain和实际的chain对不上,说话就是错误的。
代码方面:
需要修改的地方是,加上一个数组存储边权,(虽然说是边,但是一般存储在某个点上来模拟)
寻根函数,每寻找一次根,都要改变它的值,即a吃b,b吃c,那么c的掌门可以归结到a上,fx存储的是a的下标,b和c的关系也需要改变到a和c的关系,这样两条边的权就变成了一条边的权,这个权如何递推是带权并查集需要思考的一个难点。注意这个时候也是有路径压缩的。
然后是join函数,需要添加两个掌门,权如何合并的问题,即两个不同的掌门如何合并到一起,它的边权如何更新? 只需要根据题里给的两个掌门之间的权,然后链接边即可。
还有一道带权并查集的题,很可惜由于我没学离散化,暂时码不出来,但是可以用带权并查集来做
P5937 [CEOI1999] Parity Game - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
总结一下带权并查集:在一个平面内的点的连通性需要考察,点的关系也需要考察。
通过维护点之间连线的权,不仅可以知道他们是有关系的,还可以通过权来指定访问这个关系到底是什么。
需要注意的是点如何连接,掌门如何合并。这个过程中权的关系是怎么递推的
常见的询问方法就是,判断这个人说了几句假话,一般来说,前几句是条件(即给出这个图的状态)。后几句是查询(点与点的关系是不是跟图一样?)通过并查集很容易就能做出来。
感谢并查集让我a了第一道蓝题