A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 416679828 初级黑马   /  2015-2-22 15:41  /  958 人查看  /  1 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

继续数据结构的复习,本次的专题是:并查集。

      并查集,顾名思义,干的就是“并”和“查”两件事。很多与集合相关的操作都可以用并查集高效的解决。

       两个操作代码:
       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++;
          }
       }
       其中Find()函数用了路径压缩优化,而Merge()函数用了启发式合并的优化(个人感觉有了路径压缩,启发式合并优化的效果并不明显,而经常因为题目和代码的限制,启发式合并会被我们省略)。

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

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



评分

参与人数 1技术分 +1 收起 理由
lwj123 + 1 可以优化一下你的排版,这样只有你一个人看.

查看全部评分

1 个回复

倒序浏览
哥们,格式调整下哟。。
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马