并查集&&向量偏移

/*************并查集_向量偏移:(by:hnust_logch)******/

http://blog.csdn.net/logch/article/details/52996812

1.什么叫并查集的向量偏移法?//这个叫法来源于网络

并查集是表示集合关系的数据结构...
它当然可以用来表示两个元素之间是否具有某种特定的关系,但是当关系稍微复杂且多的时候,难道你也要搞很多个并查集吗?    --不然嘞?  Orz...
其实,我们可以用一个普通的并查集来表示两个元素是否具有关系, 这个关系不是特定的, 而是某些特定关系的并集.也就是说,如果两个元素处于同一个并查集,就表明他们之间存在特定的可以确定的关系; 否则,这两个元素之间没有已经定义过的关系Orz...
那么,我们怎么知道两个有特殊关系的元素之间的关系究竟是哪一种特殊关系呢?
这就是这节课的重点--向量偏移.
为此, 我们再在普通并查集的基础上引入一个数组relationship[i],它表征i与其父亲的关系距离(偏移量).等等,什叫关系距离?  简单来解释下:想象有一群同学围成一圈, a同学和在他左边的同学的距离是1,那么a和在他左边同学的左边同学的距离是2,同样,他和在他右边同学的右边同学的距离也是2. 再假设, 如果同学总数(包括a)是2.那么,在mod2意义上, a和在他左边同学的左边同学的距离是0,也就是他自己,右边雷同Orz...
所以, 这个relationship[i]的意义在这里就可见一斑了.
但是,值得注意的是: 这类并查集的变形一般适用于"分组类型的并查集",且分组(集合)不多,但是组间关系复杂的情况.//为什么?

也可以用这句话(源于网络)去理解:“陈冠希是谢霆锋情人的情人...谢霆锋是男人,所以陈冠希也是男人。”//为什么?

2.存在的问题?
a.合并时, 如何修改relationship?
b.查找时, 存在路径压缩的问题, 如何处理relationship?
c.如何判断两个已经存在的元素之间relationship是否合理?
见图…
图片给出向量和几条相关的简单公式,具体实现见代码

3.例题:

a.poj1703: Find them, Catch them
    题意:
            有两个帮派,龙帮和蛇帮, n个罪犯,从1~n编号.现在给出m条询问or信息:
         D a b :
                 a和b是属于敌对的关系(也就是属于不同帮派)
         A a b :
                 询问a和b是否属于同一个帮派, 是则输出"In the same gang.",
                 否则输出"In different gangs.",不能确定则输出"Not sure yet.".
/***************************************************
http://poj.org/problem?id=1703
Find them, Catch them
***************************************************/



/************************ 1.普通并查集法 ***********************/
#include <iostream>
#define LL long long
#include <map>
#include <cstring>
#include <cstdio>
using namespace std;
const int maxn=100100;
int par[maxn];
int hig[maxn];
int sz[maxn];//仇人编号
void prepare(int n)
{
    for (int i=0;i<=n+1;i++)
    {
        par[i]=i;
        hig[i]=1;
    }
}
int findp(int x)
{
    return x==par[x]?x:par[x]=findp(par[x]);
}
int unit(int a,int b)
{
    a=findp(a);b=findp(b);
    if(a==b)
        return 0;
    if(hig[a]<hig[b])
    {
        par[a]=b;
    }
    else
    {
        par[b]=a;
        if (hig[a]==hig[b])
            hig[a]++;
    }
    return 1;
}
int main()
{
    int t;
    scanf("%d",&t);
    while (t--)
    {
        memset(sz,0,sizeof sz);
        int n,m;
        scanf("%d %d",&n,&m);
        prepare(n);
        int a,b;
        char ch;
        for (int i=1;i<=m;i++)
        {
            getchar();
            ch=getchar();
            scanf("%d %d",&a,&b);
            if (ch=='A')
            {
                if (findp(b)==findp(a))
                {
                    printf("In the same gang.\n");
                }
                else if(findp(a)==findp(sz[b])||findp(b)==findp(sz[a]))//容易出错
                {
                    printf("In different gangs.\n");
                }
                else//条件不清晰
                {
                    printf("Not sure yet.\n");
                }
            }
            else
            {
                if (sz[a])
                {
                    unit(sz[a],b);
                }
                if (sz[b])
                {
                    unit(sz[b],a);
                }
                sz[a]=b;sz[b]=a;
            }
        }
    }
    return 0;
}




/********************** 2.向量偏移法 *********************/
#include <iostream>
#define LL long long
#include <map>
#include <cstring>
#include <cstdio>
using namespace std;
const int maxn=100100;
int par[maxn];
int off[maxn];//off[i]表征i与par[i]的关系"距离"
void prepare(int n)
{
    memset(off,0,sizeof off);
    for (int i=0;i<=n+1;i++)
    {
        par[i]=i;
    }
}
int findp(int x)
{
    if (x==par[x])
        return x;
    int pa=par[x];//先保存下前任父亲,等下要用
    par[x]=findp(par[x]);//递归, 更新现任父亲
    off[x]=(off[x]+off[pa])%2;//回溯过程更新off[x]
    //等号右边:off[pa]已经在上一层计算出来了,off[x]为x与其前任父亲的关系距离
    //等号左边:off[x]为x与其现任父亲(也就是根)的关系距离
    return par[x];//根
}
int unit(int a,int b)
{
    int ra=findp(a),rb=findp(b);
    if(ra==rb)
        return 0;
    par[ra]=rb;//固定了合并方式
    off[ra]=(2-off[a]+1+off[b])%2;
    //合并两棵树, 计算出作为子树的根节点的off,公式由向量关系很容易推得
    return 1;
}
int main()
{
    int t;
    scanf("%d",&t);
    while (t--)
    {
        int n,m;
        scanf("%d %d",&n,&m);
        prepare(n);//预处理
        int a,b;
        char ch;
        for (int i=1;i<=m;i++)
        {
            getchar();
            ch=getchar();
            scanf("%d %d",&a,&b);
            if (ch=='A')//判断关系
            {
                if (findp(b)!=findp(a))//不在同一个集合,表明还没有确定关系
                {
                    printf("Not sure yet.\n");
                }
                else
                {
                    if (off[a]!=off[b])//因为只有0和1这两种关系集合
                    {
                        printf("In different gangs.\n");
                    }
                    else
                    {
                        printf("In the same gang.\n");
                    }
                }
            }
            else//建立关系纽带
            {
                unit(a,b);
            }
        }
    }
    return 0;
}


b.poj1182:食物链
    题意:
            动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
        现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
        有两种对这N个动物的关系进行描述:
        1."1 X Y",表示X和Y是同类。
        2."2 X Y",表示X吃Y。
        对N个动物,有K句话,有真有假。当一句话满足下列三条之一时,这句话就是假话,否则就是真话;

/***********************************************
http://poj.org/problem?id=1182
食物链
ps:挑战程序设计第88页例题3设置了3个并查集
***********************************************/



/************** 1.挑战程序设计做法 ***********/
//设置三个并查集,不知道有没有敲错,以挑战为准

int N, K;
int T[MAXN_K], X[MAX_K], Y[MAX_K];

//在这里省略了并查集部分的代码
void solve()
{
    //元素x, x + N, x + 2 * N分别代表x属于A, x属于B, x属于C;
    init(N*3);
    int ans=0;
    for (int i=0; i<K; i++){
        int t=T[i];
        int x=X[i] - 1, y = Y[i] -1;//把输入变成0~N-1的范围

        //不正确的编号
        if (x<0 || N <= x || y < 0 || N <= y){
            ans++; continue;
        }

        if (type==1){
            if(same(x,y + N) || same(x, y + 2 * N)){
                ans++;
            }
            else{//对称地添加信息:分别假设x,y属于A,B,C;
                unite(x, y);
                unite(x + N, y + N);
                unite(x + 2 * N, y + 2 * N);
            }
        }
        else{
            //"x吃y"的信息
            if(same(x, y) || same(x, y + 2 * N)){
                ans++;//提取信息是可以假设x属于A or B or C的,这里总是假设x属于A;
            }
            else{//对称合并
                unite(x, y + N);
                unite(x + N, y + 2 * N);
                unite(x + 2 * N, y);
            }
        }
    }

    printf("%d\n", ans);
}






/************* 2.向量偏移法 ***********************/
#include <cstdio>
#include <cstring>

using namespace std;

const int maxn=50100;
int N, K, rel, a, b;
int par[maxn], relation[maxn];//relation[i]表征i与par[i]的关系"距离"
int Find(int x)
{
    if (x == par[x]) return x;
    int root = Find(par[x]);
    relation[x] = (relation[par[x]] + relation[x]) % 3;//回溯过程计算relation[x];
    //等号右边:relation[par[x]]已经计算好了, relation[x]是x与前par[x]的关系,
    //等号左边:relation[x]是x与新par[x]的关系,新par[x]就是路径压缩后的root.
    return par[x] = root;//更新路径压缩后的par[x]
}
void unit(int a, int b)
{
    int aa = Find(a), bb = Find(b);
    //每次unit都会先进行路径压缩,所以合并完成后树的高度最大是3
    if (aa == bb) return ;//可要可不要,不会执行
    par[aa] = bb;//固定了合并方式
    relation[aa] = (3 - relation[a] + (rel-1) + relation[b]) % 3;
}
void init()
{
    memset(relation,0,sizeof relation);
    for (int i=1;i<=N;i++)
        par[i]=i;
}
int main()
{
    //while (scanf("%d%d",&N,&K)==2)
    //{//不知道为什么,poj上用多组输入方式会wa,知道原因的麻烦告知
        scanf("%d%d",&N,&K);
        init();
        int res=0;
        for(int i=1;i<=K;i++)
        {
            scanf("%d%d%d",&rel,&a,&b);
            if (a>N||a<1 || b>N||b<1 || (rel==2&&a==b)){
                res++;continue;
            }

            if (Find(a) == Find(b)){//a和b是已经存在关系了的
                if ((rel-1) != (relation[a]-relation[b]+3)%3) res++;//a和b关系前后矛盾
                continue;
            }
            unit(a,b);//建立a和b的rel关系纽带
        }
        printf("%d\n",res);
   // }
    return 0;
}

4.复杂度:
和普通并查集差别不大。

5.建议:
a.去搜下相关资料加深理解;
b.AC几道相关题目;//自己去网上搜
c.对并查集,建议掌握另一个的知识点:删除节点//简单,网上有资料

/*******************end***********************/

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值