带权并查集

带权并查集用于解决一个集合内元素间的相互关系的查询问题。

例题1:hihocoder1515

#1515 : 分数调查

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

描述

小Hi的学校总共有N名学生,编号1-N。学校刚刚进行了一场全校的古诗文水平测验。  

学校没有公布测验的成绩,所以小Hi只能得到一些小道消息,例如X号同学的分数比Y号同学的分数高S分。  

小Hi想知道利用这些消息,能不能判断出某两位同学之间的分数高低?

输入

第一行包含三个整数N, M和Q。N表示学生总数,M表示小Hi知道消息的总数,Q表示小Hi想询问的数量。  

以下M行每行三个整数,X, Y和S。表示X号同学的分数比Y号同学的分数高S分。  

以下Q行每行两个整数,X和Y。表示小Hi想知道X号同学的分数比Y号同学的分数高几分。  

对于50%的数据,1 <= N, M, Q <= 1000  

对于100%的数据,1 <= N, M, Q<= 100000 1 <= X, Y <= N -1000 <= S <= 1000

数据保证没有矛盾。

输出

对于每个询问,如果不能判断出X比Y高几分输出-1。否则输出X比Y高的分数。

样例输入

10 5 3  
1 2 10  
2 3 10  
4 5 -10  
5 6 -10  
2 5 10  
1 10  
1 5  
3 5

样例输出

-1  
20  
0

思考1:由于输入的X、Y不一定和要查询的X、Y有交集,因此本题的目的旨在通过已知的x1,y1以及y1和z1的关系推导出x1和z1的关系。说到关系就联想到边,同时因为本题的输入说的是X比Y高,因此解题可采用有向图,用A->B表示A比B高,对于一条边要建出正反两条边,权值互为相反数。于是当我们查询P->Q时就相当于在目前的有向图上找一条P->Q的路径,将权值累加起来即可,当不存在路径时,则表示根据已有条件推不出关系。

思考2:这题的难点在于要通过已有的邻近关系推断出相距较远的关系。上一个思路的问题在于,每一次查询都要搜索一次路径,搜索的代价是O(n)的【BFS】。这里就可以用到我们的带权并查集了,我们用fa[i]表示i的父亲为fa[i],用v[i]表示i和fa[i]的关系,这题中是比父亲大多少。示意图:

为了表示大小关系,采用有向线段表示,此题中,若想知道C比E大多少,则可以通过C比A大多少以及E比A大多少求出,两者差值即为所求值。倘若是在根的同一棵子树内,例如C、D,也可以同样通过对二者与根节点A的关系得到。虽然可以通过B得到,方案是求出最近公共祖先,但是为了简化问题,同时对于带权并查集我们也将介绍路径压缩方案。

带权并查集的路径压缩:

int findF(int l) {
    if (fa[l] != l) {
        int temp = fa[l];//原来的父亲 
        fa[l] = findF(fa[l]);
        v[l] += v[temp];//由于递归回来 因此原父亲到树根的权值已经清算完成
    }
    return fa[l];
}

AC代码:

#include "pch.h"
#include<iostream>
using namespace std;
int fa[100005], v[100005];

int findF(int l) {
    if (fa[l] != l) {
        int temp = fa[l];//原来的父亲 
        fa[l] = findF(fa[l]);
        v[l] += v[temp];
    }
    return fa[l];
}

int main() {
    int n, m, k, x, y, s;
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++) {
        fa[i] = i;
        v[i] = 0;
    }
    for (int j = 1; j <= m; j++) {
        cin >> x >> y >> s;
        int f1 = findF(x);
        int f2 = findF(y);
        if (f1 != f2) {
            fa[f1] = f2;
            v[f1] = s - v[x] + v[y];
        }
    }
    for (int r = 1; r <= k; r++) {
        cin >> x >> y;
        int f1 = findF(x);
        int f2 = findF(y);
        if (f1 != f2) {
            cout << "-1" << endl;
        }
        else cout << v[x] - v[y] << endl;
    }
    return 0;
}

 

poj1182:

食物链

Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 96113 Accepted: 28990

Description

动物王国中有三类动物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句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。 
1) 当前的话与前面的某些真的话冲突,就是假话; 
2) 当前的话中X或Y比N大,就是假话; 
3) 当前的话表示X吃X,就是假话。 
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。 

Input

第一行是两个整数N和K,以一个空格分隔。 
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。 
若D=1,则表示X和Y是同类。 
若D=2,则表示X吃Y。

Output

只有一个整数,表示假话的数目。

Sample Input

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

Sample Output

3

思考1:如果我们可以准确的建立出三个并查集分别表示三种动物的话,那么,对于假话的第一个条件,我们先强制设置为真话,如果这样做使得最终的并查集个数要增加,那么我们就可以推断出此话为假。

思考2:思考1的缺陷是,准确建立出三个并查集或者说找到三个并查集的根节点并不是很容易的事情。然而此题巧妙之处在于三种动物的食物链恰好构成一个环(模运算),在带权并查集中:

A.rank=0表示儿子和父亲属于同类

B.rank=1表示父亲吃儿子

C.rank=2表示儿子吃父亲的父亲

画图说明:

A->B表示A吃B,由题意,食物链形成环。假设C->A的线不存在,->表示父亲到儿子,则上图完全符合上述权值规则。

辅助理解:经过了路径压缩之后,所有的关系被关联到了根节点上,rank表示的父子间关系不再成立,但是可以通过根节点得到其他对之间的关系。图解如下:

路径压缩之后变为:

此时A和C之间的关系并不成立,但是B和C的关系可以以A为媒介获得,(RankC-RankB+3)%3=1,因此我们知道C做儿子,B做父亲的时候C的权值为1,B吃C,与原情况相符。

AC代码:由于输入数据量较大,在poj上即使关闭了流同步依旧会TLE

#include<cstdio>
using namespace std;
int N, K, D, X, Y, F[50005], Rank[50005];

int find(int Lo) {
    if (F[Lo] != Lo) {
        int p = find(F[Lo]);
        Rank[Lo] = (Rank[Lo]+Rank[F[Lo]]) % 3;
        F[Lo] = p;
    }
    return F[Lo];//将计算好的父亲返回
}

int Join(int x,int y,int d) {
    int fx = find(x);
    int fy = find(y);
    if (fx == fy) {//已经加入并查集 可以判断正误
        if ((Rank[y] - Rank[x] + 3) % 3 != d){
            return 1;//已经经过路径压缩 Rank表示与根的差值 建立并查集时关系已经倒置
        }
    }
    else {//未加入 也通过了判断  加入并查集
        F[fy] = fx;
        Rank[fy] = (Rank[x]-Rank[y]+d+3)%3;
    }
    return 0;
}

int main() {
    scanf("%d%d", &N, &K);
    int ans = 0;
    for (int i = 1; i <= N; i++) {
        F[i] = i;
    }
    while (K--) {
        scanf("%d%d%d", &D, &X, &Y);
        if (X > N || Y > N) {//编号越界 假话
            ans++;
        }
        else if(D==2&&X==Y){//自己吃自己 假话
            ans++;
        }
        else ans += Join(X,Y,D-1);
    }
    printf("%d\n", ans);
    return 0;
}

 

poj1984  Navigation Nightmare

大致题意:给出M个点对,每次给出的点对的两点之间的关系只可能是N、S、E、W,现在有查询,每次查询给出点对X、Y以及用到了前L个条件,要求返回这两个点的曼哈顿距离。

思路:

(1)我们应该将查询先按照所用到的条件个数排序(即按照条件逐渐构成最终的判定树),以免去重复建立条件树的过程。

(2)要判定当前条件是否可知两个点的曼哈顿距离,我们知道,如果曼哈顿距离可知,那么必定这两个点对直接或者间接通过别的点进行了相连,因此如果我们对具有相连关系的点做一个集合的话,很容易可知,这就是个带权并查集。

(3)并查集的权值设为什么?如果直接设为曼哈顿距离的话,显然是有问题的,例如下图:

很显然A-B曼哈顿距离是无法通过A-C、C-D之间的曼哈顿距离通过加减得到的,原因是曼哈顿距离并不具有可加性,即A-B!=A-C+C-B。但x、y还是具有可加性的,因此我们对每一个位置还是要分别保存Vx、Vy。并且我们要按照X-Y坐标系规定正负,代码中所建立的坐标系为N正E正。

(4)路径压缩+合并根节点

路径压缩:我们用Vx[son]表示son.x-fa.x,Vy[son]表示son.y-fa.y,于是权值更新直接累加,表示的是儿子以父亲为原点的相对位置。

合并根节点:为了方便,我们直接将x的父亲接在y的父亲上(即f1以f2为父亲,当然顺序无所谓,也可以倒过来)。下图中以N方向为例,假设给定数据为x y w N。

由于我们是儿子以父亲为原点,因此我们可以用向量来表示儿子与父亲间的关系,如上图所示,那么向量f2->f1=f2->Y+Y->X+X->f1。依据这个去更新Vx[f1],Vy[f1]。V[X]表示的是X->f1,V[Y]表示的是Y->f2,因此N的更新公式为:

X:Vx[f1]=-Vx[X]+Vx[Y];

Y:Vy[f1]=-Vy[X]+w+Vy[Y];

其他的类似,不再举例。注:题目中的N和S似乎表反了,但并不影响结果,因为曼哈顿距离是差值的绝对值和。

AC代码:

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
int pre[40005],Vx[40005],Vy[40005],f1,f2,w,X,Y;

struct Farm{//定义农场的数据 
    int F1,F2,L;
    char dir; 
}; 

struct Query{//查询的结构 
    int F1,F2,index,squ,result;//  index:条件的索引   squ:原输入顺序  
};

Farm farm[40005];
Query query[10005];

int find(int L){//找父亲+路径压缩  Vx表示距根节点的距离,左负右正  下负上正 
    if(pre[L]==L){return L;} //找到父亲直接返回 
    int temp=find(pre[L]);//先存下根节点 
    Vx[L]+=Vx[pre[L]];//更新到根的距离 x 
    Vy[L]+=Vy[pre[L]];//更新到根的距离 y
    pre[L]=temp;//直接连接到根 
    return pre[L];//返回根节点 
}

void Union(Farm A){
    X=A.F1;
    f1=find(X);
    Y=A.F2;
    f2=find(Y);
    w=A.L;
    pre[f1]=f2;//以y所在分支为顶点
    switch(A.dir){//被减为根  因为是求相对值 
        case 'N':Vx[f1]=-Vx[X]+Vx[Y];
                Vy[f1]=-Vy[X]+w+Vy[Y];
                break;
        case 'S':Vx[f1]=-Vx[X]+Vx[Y];
                Vy[f1]=-Vy[X]-w+Vy[Y];
                break;
        case 'E':Vx[f1]=-Vx[X]+w+Vx[Y];
                Vy[f1]=-Vy[X]+Vy[Y];
                break;
        case 'W':Vx[f1]=-Vx[X]-w+Vx[Y];
                Vy[f1]=-Vy[X]+Vy[Y];
                break;
    }
}

int Figure(Query A){
    f1=find(A.F1);
    f2=find(A.F2);
    if(f1!=f2){
        return -1;
    }
    else{
        return abs(Vx[A.F1]-Vx[A.F2])+abs(Vy[A.F1]-Vy[A.F2]);
    } 
}

bool cmp(Query A,Query B){//按条件的索引排序 
    return A.index<B.index;
}

bool cmpNew(Query A,Query B){//按询问先后排序 
    return A.squ<B.squ;
}

int main(){
    int N,M,K,x,y,w,i,index,start=1;
    char dir;
    cin>>N>>M;//N:农场个数  M:边的条数 
    for(i=1;i<=N;i++)pre[i]=i;//初始化父亲 
    for(i=1;i<=M;i++){//输入边  并保存 
        cin>>x>>y>>w;
        cin>>dir;
        farm[i]=Farm{x,y,w,dir};
    }
    cin>>K;
    for(i=1;i<=K;i++){
        cin>>x>>y>>index;
        query[i]=Query{x,y,index,i,-1};
    }
    sort(query,query+K,cmp);
    for(i=1;i<=K;i++){
        for(;start<=query[i].index;start++){
            Union(farm[start]); 
        }
        query[i].result=Figure(query[i]);
    }
    sort(query,query+K,cmpNew);
    for(i=1;i<=K;i++){
        cout<<query[i].result<<endl;
    }
    return 0;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值