Union find,union查询语句

  Union find,union查询语句

  Union-Find是解决动态连接问题的一种非常有效的数据结构。在本文中,我将尽力用最简单明了的逻辑展示并集的构造过程,同时对关键步骤给出相应的Python代码。

  动态连通性可以想象成一张地图上有很多点,有些点是用道路连接的,有些点不是。如果我们现在是从A点到B点,那么一个关键问题就是我们能不能从A点到B点?换句话说,A和B是否相连。这是动态连接的最基本需求。现在给定一组数据,其中每个元素都是一对“点”,也就是说这些点是相连的,我们需要设计一个算法,让计算机依次读取这些数据,最终确定任意两点是否相连。注意,并集涉及的动态连通性只考虑“是否连通”的二元问题,不涉及连通路径是什么。后者超出了本文的考虑范围。

  比如像下图。为了简单起见,我们用整数0~9来表示图中的10个点,然后给出两两相连的数据如下:[(4,3),(3,8),(6,5),(9,4),(2,1),(8,9),(5,0),(7)

  我们通过在图中绘制和连接这些“点对”来依次显示它们。当然,如果其中一个“右”点是连通的,比如点对(8,9)和(6,7)在连通之前就已经连通了,我们可以自动忽略这种情况。从这张图可以直观的看到,有些点对,比如0和1,是相互连通的,有些点对,比如0和9,是不连通的。现在的问题是如何让计算机读取这些数据构造一个数据结构(合并连通点),然后在这个数据结构上对一个点对是否连通(查询连通性)做出高效的判断。所以名字才一起查。

  为了实现上述功能,一个简单的思路就是分组。也就是说,我们可以把相互连接的点看作一个组。如果查询的点对在不同的组中,那么这个点对不连通,否则连通。我们先简单分析一下这个操作的具体流程,包括“合并”和“检查”。为了描述方便,我这里举个例子:比如上图的10个点。现在,让每个点的值作为它的初始组,我们可以得到下表:

  0123456789组号0123456789现在“并”的操作可以描述为:观察第一个点对(4,3),然后先找到点4和点3,发现它们属于不同的组,然后把点4和点3的组改成3(当然都变成4也可以,这是随机设计的),然后生成下表:

  0123456789组号0123356789和“检查”的操作其实是“合并”操作的第一步:找到点对中两点所在的组,看是否相同。

  也就是说,并集的两个基本运算都涉及到“按点找组”的过程,所以我们先给出一个高效的“搜索”算法。我称之为快速查找算法。

  快速查找算法设计一个高效的查询算法非常简单。从上表中,我们可以认为元素和组之间的这种一一对应关系可以通过存储键值对来实现。我直接给出Python的实现代码,很简单。

  def con_eleGroupNum(eleList): 构造元素-组号映射:param eleList:不同元素的列表:return:具有{ element:group number } result={ } num=1 for I in eleList:result[I]=numnum num=1 return result。这样,由于可以直接找到键,并通过键访问其对应的值,所以查询的复杂度为O(1)。完美!

  但是如果是合并呢?这就有点麻烦了,因为我不知道到底哪些“键”(点)对应一个“值”(组),所以需要遍历整个列表,逐个修改。假设现在有n个点,要添加的路径由m对点组成,那么“并”的时间复杂度为O(MN)。这是一个平方级的时间复杂度。显然,当数据量巨大时,平方级算法是有问题的。以下代码显示了根据点对修改组(合并)的过程。我们将每个点对元素的最小值作为新组:

  def change _ group num(pairGroupNum,eleGroupNum): connect:param pairGroupNum:元素对的两个组号:param eleGroupNum:字典的形式为{ element:group numbe r }:return:None new group num=min(pairGroupNum)for I In eleGroupNum:if eleGroupNum[I]==pairGroupNum[0]或eleGroupNum[I]==pairGroupNum[1]:eleGroupNum[I]=newGroupNum def Quick _ In现在关键在于如何通过一个点快速找到其对应的组以及该组中的所有点,并批量改变这些点的组。显然,这里设计了数据的存储和更新,所以我们考虑设计一种新的数据结构。既然简单的键值对都解决不了,那其他数据结构呢?链表,树,图?想了想,你会发现树形结构其实很合适:相比链表和图,树形结构有一个非常“显眼”的根节点,我们可以用它来表示这棵树中所有节点的分组。修改根节点相当于修改整棵树。此外,树的层次结构决定了从一个节点查询组的过程也非常高效。

  利用树形结构实现并集的算法思想可以描述如下,假设现在增加多条路径(点对):

  初始化:每个点被视为一棵树。当然,这是一棵只有根节点的树,这个节点本身的值作为一个组存储(也可以做其他不会引起冲突的标记作为组);

  查询:对于点对(A,B),通过A和B追溯到其根节点(当然最开始是自己),判断其所在的组;

  合并:如果不在同一个组中,使一个点(如A条)所在的树的根节点成为另一个点(如B条)的根节点的子节点。即使这样再次查询A,通过上面的查询过程,程序也会最终确定B的根节点现在所在的组,相当于改变了A所在的组中所有元素的组;

  这样,计算过程中的所有树实际上都是一棵多分支树。但是我们发现又出现了一个新的问题,就是查询算法也发生了变化,使得快联的查询处理效率显得不太尽如人意。即使在最坏的情况下,这样的多分支树也会退化成链表(不具体说,你想想应该就能明白了)。

  但是没办法,因为如果要求根,复杂度一定是O(H),其中H是树的高度。再想想,能不能尽量降低这里的树高?也就是说,在生成和合并树的时候,尽量让每棵树都“平坦”。

  大树小树的合并技巧先看看树的合并,对于我们上面说的这种合并(一棵树的根直接变成另一棵树根的孩子),有一个基本的原则是小树变成大树的子树,会比大树变成小树的子树更加不易增加树高,这一点通过下面的图就能看出来。所以我们可以在生成树的时候,令根节点存储一个属性体重,用来表示这棵树所拥有的节点数,节点数多的是"大树",少的就是"小树"。

  压缩路径

  再看看树的生成,这一点就要在查询过程中做文章了,因为每次我们是从树的一个节点回溯到其根节点,所以一个最直接的办法是,将这条路径上的所有中间节点记录下来,全部变成根节点的子节点。但是这样一来会增加算法的空间复杂度(反复开辟内存和销毁)。所以一个备选的思路是每遍历到一个节点,就将这个节点变成他的爷爷节点的孩子(和其父节点在同一层了)。相当于是压缩了查询的路径,这样,频繁的查询当然会导致树的"扁平化"程度更彻底。

  代码展示经过上面大小树的合并原则以及路径的压缩,其实"并"和"查"两种操作的时间复杂度都非常趋近于O(1)了。下面我给出一些关键函数的代码。完整的代码参加我的开源代码库主页:https://github。com/guozifengbupt/Union-Find

  class UFTreeNode(object):def _ _ init _ _(self,num): #组号self.num=数字#其子self.children=[] #其父self.parent=None #以此节点为根的节点数自我。weight=1 def gen nodelist(eleList): 生成每个元素的节点:参数元素列表:元素列表:返回:一个字典,格式为{元素:对应的节点} 对于eleList中的ele,result={:result[ele]=UFTreeNode(ele)return def loc pair(ele,eleNodeMap): 定位该对元素的位置:param elePair:param eleNodeMap:一个字典,格式为{元素:对应的节点}:返回:该对元素的两个节点 return [eleNodeMap[elePair[0]],eleNodeMap[elePair[1]]]def回溯(节点): 1 .求节点2的根。削减树的高度:param节点:返回:节点的根 root=node while root。parent:cur=root root root=root。父#当前的祖父节点存在如果诅咒。父母。父级:#制作cur的父亲是它的祖父=当前父级父级祖父。孩子。append(cur)return root def quick union(ele pair,eleNodeMap): union process:param ele pair:param eleNodeMap:return: node pair=loc pair(eleNodeMap)root _ 1,root_2回溯(节点对[1]) #如果该对的两个元素不属于同一根(组)如果根_1不是根_2:如果root_1.weight=root_2.weight: #更新权重root_1.weight=root_2.weight #使root2成为root1的子树root _ 1。孩子。追加(根_2)#更新根_ 2的组号root _ 2。num=root _ 1。num else:root _ 2。权重=根_ 1。权重根_ 2。孩子。附加

Union find,union查询语句