并查集精讲

今天讲讲并查集:

首先,我们来看看并查集的原始思路:

所谓并查集,由并(union),查(find),集(set)三部分构成。适用于较大数据、复杂关系的题目,如:
给你许多关系,每行两数M a b,代表两人是亲戚;或Q a b,问两人是否是亲戚?
如果数据大的话,暴力就不行了。所以我们用到并查集。(我作为一个吃货,特别爱叫它 饼查集)
并查集是树形结构,它是一种有向图,无环。
例如:
有一father[]数组,记录节点的爸爸(上一节点)。
初始化:
for i=1→n
  father[i]=i;
操作:
1.union:
x=find(u),y=find(v);
father[y]=father[x];//将y连到x上
cnt[x]+=cnt[y];//同时也可以记个cnt-该节点所拥有的“子孙后代”数量(包括自己),在需要时使用。
2.find:
find(u)//可使用递归
  if(father[u]==u) return u;//根据初始化,根的爸爸是自己(呃......)
  else return find(father[u]);//一层一层往上递归
3.set
没有。。。set在这里是个名词。。。
这里说一下union里第一行为什么要找根。因为如果两节点直接合并,那么:
每一节点只有一个爸爸(前驱),所以把v连到u上,v与y的亲戚关系就没了。所以必须要连根。

那么,刚刚的题目思路就是:
1.读入-如果是'M',将a与b合并(先假设b的根合并到a的根上);
2.如果是'Q',检查两者是否在同一棵树上-根是否相同
ok,大家可以自己去写写。
给个数据范围:人数≤100000,信息数≤200000。
我测了一下,T(超时)了50%。
为什么呢?
来,给个数据:
100000 200000
M 1 1//强迫症专用
M 2 1
M 3 2
...
M 100000 99999
Q 1 100000
...
Q 1 100000
好了,嗯,时间:100000*100000=1^10
尴尬
呵呵。所以必须优化。
看上面的数据,
每一次询问都是一样的。所以死板地find很傻。
可以不那么傻吗?当然。
在查询一次后把树变成这样。
的确可行。我们把它叫做路径压缩。
伪代码:
find(u)
  if(father[u]==u) return u;
  else
    father[u]=find(father[u]);
    return father[u];
可简写为一行:
  return father[u]==u ? u : father[u]=find(father[u]);
这样就好了。
但是-如果给你足够大的数据。。。
那么第一次递归就会让你爆掉——爆栈。(甚至直接超时)
所以就有另外一种办法叫按秩合并。
秩就是刚刚提到的由cnt数组记录的东西,也就是子孙后代的数量。
那么我们这里把它叫做rank,秩的英文。
将秩小的合并到秩大的上,这样树就变得“扁”了。
具体代码:
union(u,v)
  int x=find(u),y=find(v);
  if(x==y) return;
  if(rank[x]==rank[y]){
    f[y]=x;
    rank[x]++;//秩相同,合并到前一个上,秩加一,可以画图证实。
  }
  else if(rank[x]>rank[y])
    f[y]=x;
  else
    f[x]=y;
//秩不同,就算合并上去,也不会改变根的秩。

好,现在我们已经学了基本的操作,来看一道题:

问题: 体育场

时间限制: 1 Sec   内存限制: 128 MB

题目描述

浙江省第十二届大学生运动会在浙江师范大学举行,为此在浙师大建造了一座能容纳近万人的新体育场。 
观众席每一行构成一个圆形,每个圆形由300个座位组成,对300个座位按照顺时针编号1到300,且可以认为有无数多行。现在比赛的组织者希望观众进入场地的顺序可以更加的有趣,在门票上并没有规定每个人的座位,而是与这个圈中某个人的相对位置,可以坐在任意一行。 
门票上标示的形式如下:A B x 表示第B个人必须在A的顺时针方向x个位置(比如A坐在4号位子,x=2,则B必须坐在6号位子)。 
现在你就座位志愿者在入场口检票。如果拿到一张门票,与之前给定的矛盾,则被视为是假票,如果无矛盾,视为真票。现在给定该行入场观众的顺序,以及他们手中的门票,请问其中有多少假票? 

输入

第一行两个数N(1<=N<=50,000)和m(1<=m<=100,000)。表示N个人,m张票。 
接下来m行,每行三个数A,B,x标示B必须坐在A的顺时针方向x个位置。(1<=A<=N), B(1<=B<=N), X(0<=X<300) (A!=B) 

输出

仅一个数,表示在m张票中至少有多少假票。

样例输入

10 10
1 2 150
3 4 200
1 5 270
2 6 200
6 5 80
4 7 150
8 9 100
4 8 50
1 7 100
9 2 100

样例输出

2

提示:第5张和第10张是假票。


好,先让我们理解一下题意:
有无数圈座位,每圈300座位。圈套圈。
给你许多条件。当矛盾时ans++。
那么怎么样是假票了呢?
由于每列有无数座位,那么就不用担心座位不够的情况。
矛盾就是指给出的数据表达同一个人坐在不同的两个位置上了!也就是分身了
得意
那么怎么判断矛盾呢?我们等会来说。

首先,一定会有人说:暴力呗!假设一个人的位置是1,按基准判断。
那好,给你数据:
3
1 2 1
3 4 1
4 1 1
OK,这下你输出1还是0?

所以,我们要使用并查集!
但是,这次的并查集不一样了。边上要带权值——距离。我们设d[i]表示I在f[i](father[i])顺时针第几个位置。
1.初始化
2.读入-如果两者处于不同的树中,那么一定不会矛盾,因为两个人根本没关系嘛,题中说至少有几张假票,所以这种情况不计。
但是要把他俩合并;如果两者在同一树中,再合并就出现环啦!所以要判断矛盾。

现在,先来看看合并(合并根):
例:
这里有两棵树,f[2]=1,f[4]=3.
给数据"2 4 x".
合并find(4)到find(2).也就是合并3到1.
学过向量的人一定知道这个,这里就不赘述了。
通过这个例子,我们懂得合并的步骤是(伪代码):
root1=find(a),root2=find(b);
fa[root2]=root1;
d[root2]=(-d[b]+x+d[a])%300;//模一下

接下来,在形成环时判断矛盾:
好了,现在已经解决了大部分问题。但想想,如果树比较大,条件中的节点与根之间 隔着许多节点呢?
那么需要一个计算。
calc(x)
  if(fa[x]==x) return 0;
  return d[x]+calc(fa[x]);
但是这里还有一问题,当-d[b]+x+d[a]或d[b]-d[a]<0时,模300就无用了,无法统一。
例:d[i]=-50,也就是i在fa[i]的顺时针-50个位置=逆时针50个位置=顺时针-50+300=250个位置。(可以画图试试)
所以这里我们可以加一个300后再模。
所以总结:合并
root1=find(a),root2=find(b);
fa[root2]=root1;
d[root2]=(-calc(b)+x+calc(a)+300)%300;
判断
if((calc(b)-calc(a)+300)%300!=x%300)
  ans++;
但是——
我们可以优化find。
现在,find是没有路径压缩的,是因为边上有权值。但现在想想,可以把权值加起来啊!
来理解一下:
int find(int u){
    if(fa[u]==u) return u;
    int t=fa[u]; //暂时记着
    fa[u]=find(fa[u]); //压缩
    d[u]=(d[u]+d[t])%300; //把当前的权值与原来的爸爸的变化后权值相加,模300。可以画图验证(可能这句话有点绕)
    return fa[u];
}
这时,fa[u]一定为根,d[u]就是u与根之间的距离。所以calc就可以拜拜了。
现在,所有东西就完成了。
代码:
#include<bits/stdc++.h> 
using namespace std; 
int n,m,a,b,x,fa[50001],d[50001],root1,root2,ans; 
int find(int u){ 
    if(fa[u]==u) return u; 
    int t=fa[u]; 
    fa[u]=find(fa[u]); 
    d[u]=(d[u]+d[t])%300; 
    return fa[u]; 
} 
int main(){ 
    int i,j; 
    scanf("%d%d",&n,&m); 
    for(i=1;i<=n;i++) fa[i]=i,d[i]=0; 
    for(i=1;i<=m;i++){ 
        scanf("%d%d%d",&a,&b,&x); 
        root1=find(a),root2=find(b);
        if(root1!=root2){
            fa[root2]=root1;
            d[root2]=(-d[b]+x+d[a]+300)%300;
        }
        else if((d[b]-d[a]+300)%300!=x%300)
                ans++;    
    }
    printf("%d",ans); 
    return 0; 
} 
好的,这次饼(并)查集就讲到这了,谢谢大家!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值