[带权并查集] 学习一个

(生活有点小烦,希望在写代码的时候能够纯粹一点。)

先作一点铺垫:

从以前的认识来看,并查集(Union-Find Sets)可以实现分类的目的。

她提供两种操作:1.Find-查询根节点   2.Union-合并(两个元素间的关系将两个不同集合合并为一个,保证了关系的传递性)

值得注意的是并查集有两个优化的方法——

1. 为了防止退化成链,合并两个集合时,将高度小的的树接在高度高的树上。(卡这一点的题目我还没有遇到过...)

2. 重要的是路径压缩;我用的模版里,在查询的时候进行了路径压缩——将根节点到查询节点整条链上的结点都接到根节点上(因为是递归,所以是从根节点开始压缩路径的

(因为合并在查询之后,再怎样防止退化都不能阻止高度增加。)

(#事实上这种写法在压缩路径前任何幺蛾子结构都可能出现,数据结构的并查集应该时时保证结构高度只有两层吧?)

int find(int i){
    if(f[i]!=i) f[i]=find(f[i]);   //递归————从树根开始压缩路径
    return f[i];
}
void unin(int a,int b){
    ta=find(a);
    tb=find(b);
    if(ta!=tb) f[ta]=tb;
}

现在每个节点只有一个属性——f[i],代表的是他的父亲结点,我们每次压缩路径(更改父节点)就是在维护f[i]的值指向集合的根节点。

带权并查集就是在每个节点定义权值v[i],这个值代表的是和父节点的关系。并在1.查询(压缩路径)和2.合并 的时候维护这个值。//根据题意写更新的算式

此时,同一集合不代表同一种类,而是维护了元素间的关系。

换句话说,在同一集合的两结点,他们的关系就是已知的(根据权值判断是否错误);如果不在同一集合,那就更新关系(默认正确)。

(并查集通过合并的方法来区别不同的集合,带权并查集是一种思路,种类并查集是另一种思路)


然后现在只有一个问题,就是我如何找出一个集合中任意两点之间的关系(毕竟每个节点只存有和父亲节点的关系)——利用路径压缩,两个节点会被接到同一个根节点上(此时权值也已经更新为和根节点的关系);这样两个节点之间只有一个(或者没有)中间节点,很容易推出需要查询的两结点间的关系。


以下是两道练习题:

POJ1182 食物链

题意:

动物分成三类ABC,相互的关系是A吃B,B吃C,C吃A的循环关系;

n个动物,m次询问。 

“r a b”为一次询问,r==1时ab为同类动物,r==2a吃b

1.a、b大于n 或者 2.自己吃自己 或者 3.和已知关系冲突的询问 是错误询问,最后输出错误询问次数。


思考:

把所有关系都存起来,还能根据传递性推出任意两点间的关系,这就是关系并查集啊。需要构造的是一种循环吃的三元关系,毛估估想一下,已知两个节点对同一节点的关系是可以推出这两个节点的关系的(那就可以用带权并查集)。定义权值为“被父节点吃-2,吃父节点-1,和父节点同类-0”

之前说要在1.查询(压缩路径)和2.合并 的时候维护权值,很重要的是——我要在三层结构中完成对权值的维护。(为什么要在三层结构进行维护呢,因为查询的时候压缩路径是从根节点开始压缩的)就是说,已知和父亲的权值和父亲对爷爷的权值,将我现在对父亲的权值更新为我对爷爷的权值,因为我压缩路径直接接到爷爷结点了。我是列举了各种情况凑出了一个更新权值的公式——v[i]=(v[i]+v[f[i]])%3;  

所以,查询(压缩路径)实现如下:

int find(int i){
    if(f[i]!=i){
        int root=find(f[i]);  //先递归到根节点,保证是三层结构开始更新权值
        v[i]=(v[i]+v[f[i]])%3;
    }
    return f[i];
}
判断权值是否副符合:自己推公式吧,反正压缩路径之后就是两层的结构。

还有值得一提的是——合并操作:因为路径压缩后只有两层结构:a-->ta    b-->tb两棵树,要将ta接到tb上步骤是:求出ta对a的权值,ta接到b上,再接到tb上:

v[ta]=( ( (3-v[a])%3 + (r-1) )%3 +v[b] )%3;

AC代码:

#include <iostream>
#include <cstdio>
#define maxn 50005
using namespace std;
int n,k;
int r,a,b;
int ans;
int f[maxn],v[maxn];
void init(int n){
    for(int i=0;i<=n;i++){
        f[i]=i;
        v[i]=0;
    }
}
int find(int i){
    if(f[i]!=i){
        int root=find(f[i]);  //先递归调用到根节点
        v[i]=(v[i]+v[f[i]])%3;
        f[i]=root;
    }
    return f[i];
}
void merge(int a,int b){
    int ta=find(a);
    int tb=find(b);
    //此时经过路径压缩,a节点已经是set中深度为2的节点了
    if(ta==tb){
        if(r==1 && v[a]!=v[b]) ans++;
        else if(r==2 && (v[a]+2)%3!=v[b]) ans++;
    }
    else {
        v[ta]=( ( (3-v[a])%3 + (r-1) )%3 +v[b] )%3;
        f[ta]=tb;
    }
}

int main(){
    cin>>n>>k;
    init(n);
    ans=0;
    while(k--){
        scanf("%d%d%d",&r,&a,&b);
        if(a>n || b>n) ans++;
        else if(r==2 && a==b) ans++;
        else merge(a,b);
    }
    cout<<ans<<endl;
    return 0;
}


SHUOJ 1880

题意:

武林中共有 N 个人,编号分别为 1~N,有 M条命令,两种格式:

1 x y:编号为 x y的两个人所在的帮派合并了,的帮派的帮主归于 y 的门下,的等级比 x 的帮派的帮主高一级(如果 x  y 已经在一个帮派中,则跳过这条命令。);

2 x y:请回答编号为 x y的两个人是否是自己人。


思路:

权值为和父亲的等级差距


AC代码:

#include <iostream>
#include <cstdio>
#define maxn 100005
using namespace std;
int n,m;
int r,x,y;
int ans;
int f[maxn],v[maxn];
 
void init(int n){
    for(int i=0;i<=n;i++){
        f[i]=i;
        v[i]=0;
    }
}
int find(int i){
    if(f[i]!=i){
        int root=find(f[i]);
        v[i]+=v[f[i]];
        f[i]=root;
    }
    return f[i];
}
 
 
int main(){
    cin>>n>>m;
    init(n);
    ans=0;
    while(m--){
        scanf("%d%d%d",&r,&x,&y);
        int tx=find(x);
        int ty=find(y);
        if(r==1 && ty!=tx){
            f[tx]=ty;
            v[tx]+=v[y]-1;
        }
        else if(r==2){
            if(tx!=ty) printf("No\n");
            else if(ty==tx)
                printf("Yes %d\n",v[x]-v[y]);
        }
    }
    return 0;
}

以上,带权并查集入门吧。

今天icpc world final


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值