数据结构——并查集

特点:

实现简单,效率较高。

功能:

1,集合的快速合并;

2,快速查找某元素所在集合。

优化:

1,合并优化;

2,路径压缩。

效率:

按秩合并:时间:O(m*lg(n))

路径压缩:最坏:  O(n+lg(n))

按秩合并和路径压缩:O(mα(n))

  经过启发式合并和路径压缩之后的并查集,执行m次查找的复杂度为O(mα(n))

l其中α(n)是Ackermann函数的某个反函数,增长速度及其缓慢。α(n)<=4。所以并查集的单次查找操作的时间复杂度也几乎是常数级的。

n为所有元素个数,m为关系个数。所以,经过优化后效率可达线性。
核心函数:
       int Find(int x){
            if (tree[x].parent != x){
                 tree[x].parent = Find(tree[x].parent);  //路径优化
            }
            return tree[x].parent;
       }
       void Merge(int a, int b, int p, int q, int d){
            if (tree[q].depth > tree[p].depth) tree[p].parent = q;     //启发式合并
            else{
                  tree[q].parent = p;
                  if (tree[p].depth == tree[q].depth) tree[p].depth++;
            }
       }

典型应用:

1.      普通并查集;POJ-1611POJ-2524POJ-2236

2.      种类并查集POJ-1703POJ-2492POJ-1182POJ-1733POJ-1988POJ-1417


举例:

 POJ 1182 食物链
       http://acm.pku.edu.cn/JudgeOnline/problem?id=1182
       题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)
这题有几种做法,我以前的做法是每个集合(或者称为子树,说集合的编号相当于子树的根结点,一个概念)中的元素都各自分为A, B, C三类,在合并时更改根结点的种类,其他点相应更改偏移量。但这种方法公式很难推,特别是偏移量很容易计算错误。
下面来介绍一种通用且易于理解的方法:
首先,集合里的每个点我们都记录它与它这个集合(或者称为子树)的根结点的相对关系relation0表示它与根结点为同类,1表示它吃根结点,2表示它被根结点吃。
那么判断两个点a, b的关系,我们令p = Find(a), q =Find(b),即p, q分别为a, b子树的根结点。
       1.
如果p != q,说明a, b暂时没有关系,那么关于他们的判断都是正确的,然后合并这两个子树。这里是关键,如何合并两个子树使得合并后的新树能保证正确呢?这里我们规定只能p合并到q(刚才说过了,启发式合并的优化效果并不那么明显,如果我们用启发式合并,就要推出两个式子,而这个推式子是件比较累的活所以一般我们都规定一个子树合到另一个子树)。那么合并后,prelation肯定要改变,那么改成多少呢?这里的方法就是找规律,列出部分可能的情况,就差不多能推出式子了。这里式子为 : tree[p].relation = (tree[b].relation – tree[a].relation +2 + d) % 3; 这里的d为判断语句中a, b的关系。还有个问题,我们是否需要遍历整个a子树并更新每个结点的状态呢?答案是不需要的,因为我们可以在Find()函数稍微修改,即结点x继承它的父亲(注意是前父亲,因为路径压缩后父亲就会改变),即它会继承到p结点的改变,所以我们不需要每个都遍历过去更新。
       2.
如果p = q,说明a, b之前已经有关系了。那么我们就判断语句是否是对的,同样找规律推出式子。即if ( (tree[b].relation + d + 2) % 3 != tree[a].relation ), 那么这句话就是错误的。
       3.
再对Find()函数进行些修改,即在路径压缩前纪录前父亲是谁,然后路径压缩后,更新该点的状态(通过继承前父亲的状态,这时候前父亲的状态是已经更新的)
      
核心的两个函数为:
       int Find(int x)
       {
           int temp_p;
          if (tree[x].parent != x)
          {
             //
因为路径压缩,该结点的与根结点的关系要更新(因为前面合并时可能还没来得及更新).
             temp_p = tree[x].parent;
             tree[x].parent = Find(tree[x].parent);
             // x
与根结点的关系更新(因为根结点变了),此时的temp_p为它原来子树的根结点.
             tree[x].relation = (tree[x].relation + tree[temp_p].relation) % 3;
          }
          return tree[x].parent;
       }

      void Merge(int a, int b, int p, int q, int d)
       {
          //
公式是找规律推出来的.
          tree[p].parent = q; //
这里的下标相同,都是tree[p].
          tree[p].relation =(tree[b].relation – tree[a].relation + 2 + d) % 3;
       }

      而这种纪录与根结点关系的方法,适用于几乎所有的并查集判断关系(至少我现在没遇到过不适用的情况可能是自己做的还太少了…),所以向大家强烈推荐~~



程序代码:

#include <stdio.h>
#include <iostream>
using namespace std;

struct point
{
    int parent;
    int kind;
} ufind[50010];

void init(int n);

int find(int index);

void unions(int rootx, int rooty, int x, int y, int dkind);

int main()
{
    int n,k,count=0,d,x,y;

	scanf("%d%d",&n,&k);
    
        init(n);
        while(k--)
        {
            scanf("%d%d%d",&d,&x,&y);
            if(x>n || y>n)
                count++;
            else if(d==2 && x==y)
                    count++;
            else
            {
                int rx=find(x);
                int ry=find(y);
                if( rx!=ry )
                    unions(rx,ry,x,y,d-1);
                else
                {
                    if(d==1 && ufind[x].kind!=ufind[y].kind)
                        count++;
                    if(d==2 && (ufind[x].kind-ufind[y].kind+3)%3!=1)
                        count++;
                }
            }
        }
        printf("%d\n",count);
    
    
    return 0;
}

void init(int n)
{
    for(int i=1;i<=n;i++)
    {
        ufind[i].parent=i;
        ufind[i].kind=0;
    }
}

int find(int index)
{
    int temp;
    if( index==ufind[index].parent )
        return index;
    temp = ufind[index].parent;
    ufind[index].parent = find(ufind[index].parent);
    ufind[index].kind = (ufind[temp].kind+ufind[index].kind)%3;
    return ufind[index].parent;
}

void unions(int rootx, int rooty, int x, int y, int dkind)
{
    ufind[rooty].parent=rootx;
    ufind[rooty].kind=(-dkind+(ufind[x].kind-ufind[y].kind)+3)%3;
}




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值