在POJ根据前人的分类,做了很多并查集的题目,如果知道是应用并查集的话,建模和code起来已经比较熟练了,不过不知在遇到陌生题时是否能发现它是用并查集来完成。今天来总结下(要在做了些并查集的题目后才能理解)。做得题目有1182,1611,1703,1988,2236,2492等等,还有一些很基础的。
我是从食物链那道经典题入手的(
http://www.cppblog.com/tortoisewu/archive/2009/05/23/85501.html),读懂别人写的代码后就基本理解并查集的组织与应用了,因为这道题很典型也比一般的复杂。
并查集一般就三个操作:初始化,查找,合并。一般都是用一个一维数组来组织(实际是一棵树,每个结点都指向父节点,用根节点来标志集合),数组所存内容为该下标表示点的父节点。
先讲下对于并查集三个操作中必须的部分:初始化函数一般没有什么变化,都是将每个结点的父节点初始化为本身;查找函数一般都是递归的寻找父节点直至根结点,返回根结点的值来标识集合;合并函数一般就将两个集合中一个根结点的父节点设为另一个根结点。
这样最基本的并查集就构成了,但是对于具体问题尤其是一些复杂问题必须要加入一些操作:
1.路径压缩
即在查找操作时,将查找的结点到根结点路径上的所有结点都挂到根结点上,这样就保证了树的高度不会很高,节省了查找时间。
2.附加类型标识(这是我的叫法哈)
就是对于每一个点还包括一个类型标识,在有些问题时是必需的(例如食物链),并查集中两个点同属于一个集合表明已明确了这两点的关系,关系通过类型标识来区分(往往是相对的),那在加入了类型标识的并查集中就必须丰富那三个基本操作:
1.首先初始化时将每个点的类型标识初始化为0,这里初始化为0我认为是最优的,这能保证每个集合的根结点的类型标识都为0,以方便后面的操作(需要具体做题来体会哈);
2.对于合并操作,要修改被合并的根结点的类型标识(即合并后它不再是根结点的点),要保证这个点的类型标识在合并后的集合中是准确的(而这个点的后继的类型标识将放在查找操作中修改,因为并查集集合是无法访问到后继的,只能由叶结点向根结点遍历),那修改的依据是什么呢?假设所给新的关系涉及两个点A,B,它们的根结点是RA,RB,那依据有三个:1.RA和A的相对关系,2.RB和B的相对关系,3.新给出的A和B的关系。这样就可以得出RA和RB的关系(至少做到的题中都能根据这三个条件推出,这是必须的,如果不能推出则要修改并查集的结构了),即RB类型标识的新值(如果是把RB所在集合合并到RA所在集合的话);
3.对于查找操作,正如前面所说,我们只要在压缩路径时,从路径中深度最低的点(这个顺序也很关键)根据当前点和当前点父节点的类型标识更新当前点的类型标识即可,这样能保证查找后,该路径上所有的类型标识都正确。那为什么根据当前点和当前点父节点的类型标识就能获得正确的类型标识呢?因为在合并时,被合并集合的根结点加入新集合后的类型标识是正确的,而因为集合根结点的类型标识都为0,所以加入新集合后原集合非根节点的类型标识加上原来其根结点的新类型标识的值即可得到正确的值,且此操作必须按深度递增的方向进行(这里很难理解,需通过实例才能明白哈,画个图能帮助理解)。
掌握以上的方法,一般并查集的题目应该很熟练了。
posted on 2009-05-29 21:48
tortoisewu 阅读(846)
评论(0) 编辑 收藏 引用