从头思考并查集

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">(学习并查集之前)</span>

   LA 3644

        题意抽象:有一些线段,按顺序插入这些线段,如果插入一条线段后形成环,则不选择这条线段。求解最后又多少条线段被放弃。

        分析:判断是否成环,到底是跟点有关系呢还是跟线段有关系呢?或者说是以点为元素还是以线段为元素呢?

                   第一直觉是以线段为元素,毕竟是由线段构成环。如果是以线段为元素,每个元素可以有“左右手”,分别为该线段左边的边和右边的边。这样加入一条线段(加入时需要查询到底要加到哪里),就判断其左端的左端的左端。。一直到没有左端,看这条线段是否恰好为新加入的线段的右端,如果是,则放弃该线段。我们只需要知道一条线段最左端的线段,所以可以压缩路径。 但是每加入边时需要查询几乎前面的所有线段。。这几乎是无法实现的。。 

                   然后我们来考虑以线段端点为元素。 点与点之间的关系是什么?相连或者不相连。如果加入一条边使两个点相连之前,这两个点已经相连的,则说明加入该点会形成环,放弃该线段。 怎样知道一个点和另一个点是否相连呢?dfs或者bfs吗?其实路径的储存是不必要的,那么路径可以压缩吗?我们并不知道后面要查询哪个点,所以似乎无法压缩。 。。假设已知A1 A2 A3 A4...An 相连,那么B与这些点中的任意一个点相连,都与其他所有的点相连。A1 A2 A3...An可以看做是一个集合。如果B与这个集合中的某个元素相连,那么B也加入这个集合。如何表示这个集合呢? 我们可以给这个集合一个代号,将B加入集合,也就是给B戴上该集合的代号。以先来后到的规则来说,可以以一个点的序号来表示。如果我们加入一条线段时,发现该线段两个端点的代号相同,则放弃该线段。不然就统一着两个集合的代号。

                  抽象 --->按照元素是否相连,给元素分类。

     

 POJ 1182 食物链

       题意抽象:有N个元素,这些元素间有三种关系,同类,左邻或右邻。现按顺序给出这些元素间的关系,如果与前面的关系矛盾,则放弃。问最后有多少条关系被放弃。

       分析:如图所示,

       

                这一次有联系的元素不能单纯的只用一个代号,因为大集合内部的元素还有关系,分为了三个小集合。第一个难点在于,如何将大集合内部的关系表述清楚呢?我们任选大集合里的一个元素为特殊点,则大集合内的其他元素与该特殊点的关系都明确了,同时我们以该特殊点为该集合的代号。按照先来后到的顺序,以第一条关系的第一个点为特殊点。这样我们描述每个元素,第一要知道所在集合的代号,第二要知道该元素与特殊点的 关系。我们对上面的状态图进行简化,如图。


                第二个难点在于,当两个大集合里的一对元素产生关系时,如何按照对应关系合并这两个大集合的元素?

                第一种想法,直接改变这两个大集合的特殊点,以发生关系的这两个点为特殊点,更新所有点的信息,这样就可以直接一一对应进行合并了。 然而要扫描所有的点,费时太多不可行。

               。。。。然后就没有想法了。。然后看了题解知道可以将每个元素与特殊点的关系定义为0,1,2.即同一集合为0,左邻为1,右邻为2,当合并时可以直接用公示推导。

               那么我们就来推公式吧(唉如果不是要写blog我就不会自己推公式了。。果然以前还是不踏实捷径走太多。。)

 


       唉推公示下了好大的决定才开始,然后静下心几秒钟就推完了T-T。 y=(x+(b-a))%3.

       啊啊啊。不对啊。这样的话其他元素更新不是还需要扫描其他所有点吗? 实际上我们不需要更新所有点的信息,因为后面的条件里可能只会出现其中的一些。我们能不能给特殊点一个附加信息,合并时更新附加信息。后面的条件里,一个元素找到它所在特殊点时,先根据这个特殊点的附加信息确定其真正所在的集合。这样只有用到的点才更新信息。这个附加信息写什么呢? '我‘合并到'XX’点了,我跟这个XX点的关系是。。” 看上去跟是不是面很熟?这不就是题目给的条件的形式嘛! 形式一样的话 ,说明它们地位应当等同,但是这里我们却把它单独拎了出来,说明前面的思路该应该再抽象一层。

     既然形式相同,我们就把前面的化简方法套用在这里,得到如图。


           这正是二叉树的形式! 而条件或者说附加信息 描述的正是该点与其父亲的关系。之所以为二叉树,是因为这种关系有两种,如果有更多种关系,那么也可以是多叉树。

          当我们要处理一个新条件时,要找到每个点与其所在树的根节点的关系。这样只要一直顺着父节点走,走到根节点即可。走的过程中应该更新与新父节点的关系。

          问题又来了。已知一节点与父节点的关系,及父节点与父节点的父节点的关系,如何知道爷孙的关系? 直接带入上面的公式y=(x+(a-b))%3,其中a-b就是父节点与父节点的父节点的关系,x是节点与父节点的关系。

          能否压缩路径呢?由于对其他节点没有影响,所以答案是可以的。途中的点如何压缩?我们可以换一种思路,由从下到上变为从上到下。之前思路是先由父子关系得到爷孙关系,再由爷孙关系得到更上一层关系。现在思路为先得到父节点与根节点的关系,再得到子节点与根节点的关系。这样就可以对途中的点也进行路径压缩了。

         需要注意的是,我们已经将与子节点与父节点的关系更新为子节点与根节点的关系了,丢失了子节点与原父节点的关系,然而这个值在进行变换时需要用到,所以需要提前记录下来。

         等等。。上图有问题。。

    

           这样的话。。上上图下面的内句话就忽视吧害羞 有几种关系应该是在倒内些公式的时候有影响。

           至此我们终于解决了这道题,并且抽象得到了并查集这种数据结构。


          现在我们再把第一题带入并查集模型。

          子节点与父节点没有附加关系,有父子关系即相连。查看两个点所在树根节点是否相同,即可判断放弃或合并。这是最简单的并查集了。


         最后来看看百度百科上对并查集的定义:

       并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。

集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。 
 代码:
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;
const int maxn=50000+10;
int n;
int pa[maxn],relation[maxn];
void init(){
 for(int i=1;i<=n;i++) {pa[i]=i;relation[i]=0;}
}

void findset(int x){
if(pa[x]==x) return ;
findset(pa[x]);
relation[x]=(relation[x]+relation[pa[x]])%3;
pa[x]=pa[pa[x]];
}


int judge(int d,int x,int y){
if(x>n||y>n)return 0;
if(x==y&&d==2)return 0;
if(d==1) {
   findset(x);
   findset(y);
   if(pa[x]==pa[y]&&relation[x]==relation[y])return 1;
   if(pa[x]==pa[y]&&relation[x]!=relation[y])return 0;
   else {
    relation[pa[x]]=(3+(relation[y]-relation[x]))%3;
    pa[pa[x]]=pa[y];
    return 1;
   }
}
 if(d==2){
    findset(x);
    findset(y);
    if(pa[x]==pa[y]){
        if((3+relation[x]-relation[y])%3==1) return 1;
        else return 0;
    }
    else{
        relation[pa[x]]=(3+relation[y]-(relation[x]+2)%3)%3;
        pa[pa[x]]=pa[y];
        return 1;
    }
 }

}


int main(){
int ans=0;
int k;
scanf("%d%d",&n,&k);
init();

for(int i=0;i<k;i++){
    int x,y,d;
    scanf("%d%d%d",&d,&x,&y);
    if(!judge(d,x,y))ans++;
    //for(int i=1;i<=n;i++)printf("%d-relation: %d  ",i,relation[i]);
}
printf("%d",ans);
return 0;
}

         (第一篇blog,写了4个多小时,想到哪写到哪。虽然实际上已经知道了并查集这种模型,但是假装自己不知道,从头开始思考问题抽象出并查集模型的过程真的收获很大。有好几次,写着写着惊觉模板里有什么东西应该在前面而我没有思考到,又想了想发现前面的思路并没有漏洞。然后接着往后写,碰到问题就解决问题,终于把模板里的所有条件都考虑到了。耗时比直接背模板多得多,但是体会比直接看模板深的多。)

          



 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值