草庐IT

连通分量(quick-union)

cheniie 2023-09-03 原文

连通域问题的抽象表述是存在N个节点和M条边,被边直接或间接相连的所有节点共同形成一个域,称为连通域。在进行有限次的连接后,需要快速求出连通域的个数,或者判断任意两个节点的连通性。连通域的个数也称为连通分量,该算法也被称为Union-Find。

例如,下图中的节点就包含三个连通域(红,黑,蓝)。

把节点看作人,把边看作关系,那么连通域就可以用来抽象人群划分问题。把点看作触点,把边看作导线,这就是电路板布线问题。同样连通域也可以用来抽象网络连接问题,用来判断网络中节点的连通性。

在不同的场景下,节点有着不同的具体表示,但是做为算法,我们可以采用更抽象的形式,用0到N-1表示N个节点。我们很容易想到可以用一个数组来表示这N个节点,但是如何在一维数组上构造连接关系需要我们动一点脑筋。

一维数组的每个元素其实都包含着两个信息,一是下标,二是值。下标是固定不可变的信息,我们用它来表示节点,而值就可以用来构造连接关系。在接下来的描述中,我们会频繁使用"节点"和"下标"这两个表述,你需要知道它们是等价的。

连接的含义是通过一个节点可以找到另一个节点。如果我们在某个下标对应的值填入另一个下标,那么就相当于在这两个下标之间建立了一个单向连接。如下图所示,这就是我们需要的全部技巧。

此时判断两个节点是否相连就转换为判断两个下标对应的值是否相等。下标对应的值是什么呢?是另一个下标,或者我们可以称它为父节点。

这样的连接实际上构成了一颗自底向上的树,我们无法从根节点找到叶子节点,但是可以从任意一个节点找到根节点,如果两个节点的根节点相同,那么它们就位于同一颗树,也就是在同一个连通域。对于根节点,我们只需要将它的值设置成它自己的下标即可,也就是让根节点指向它自己。这样,凡是下标与值相等的就是根节点,不相等就是非根节点。当然,设置成一个特殊值,如-1也是可以的。

构建一颗自顶向下的树至少需要三个域,而构建一颗自底向上的树只需要两个域,因为一个节点可以有任意个子节点,但只能有一个父节点。

下面是一个四个节点的例子,我们依次连接(3,2),(3,1)和(3,0)。

在开始之前我们需要先明确需要做什么,我们至少需要四个操作:

  • 连接两个节点:Union
  • 判断两个节点是否相连:Connected
  • 计算连通域个数:Count
  • 寻找一个节点的根节点:Find

每当我们新增一个有效连接时,都会将两个连通域合并成一个,这种二变一的结果就是相比于连接之前,连通域的个数会减少一个,而初始时,没有任何连接,有多少节点就有多少连通域。因此对于Count,我们可以记录初始连通域个数,然后在Union时更新它。就目前为止,我们至少可以写出下面的代码。

type UF struct {
  n    int
  node []int
}

func New(n int) (uf UF) {
  uf.n = n
  uf.node = make([]int, n)
  for i := 0; i < n; i++ {
    uf.node[i] = i
  }
  return
}

func (u UF) Count() int {
  return u.n
}

func (u UF) Connected(p, q int) bool {
  return u.Find(p) == u.Find(q)
}

func (u *UF) Union(p, q int) {
  if u.Connected(p, q) {
    return
  }
  //do union
  u.n--
}

func (u *UF) Find(p int) int {
  //find root of i
}

两个节点的连接是非常简单的,两个连通域的连接也不难,但是这里存在两种选择,不同的选择会带来不同的实现和性能表现。

连接两个连通域也就是合并两棵树,第一种方式是将一棵树的所有节点都挂到另一棵树的根节点上(如下图左),第二种方式是只将根节点挂到另一棵树的根节点上(如下图右)。

第一种方式产生的树只有两层,根节点和叶节点,它非常利于Find操作,但是不利于Union操作,因为Union时需要遍历一棵树。这里实际的操作是遍历数组,因为树只有两层,所以我们遍历数组一次就能找到全部节点。因此,第一种方式的实现也称为quick-find算法。以下是该算法的UnionFind的实现。

func (u *UF) Union(p, q int) {
  pRoot, qRoot := u.Find(p), u.Find(q)
  if pRoot == qRoot { //已连接
    return
  }
  //将连通域p合并到q
  for i := 0; i < len(u.node); i++ {
    if u.node[i] == pRoot {
      u.node[i] = qRoot
    }
  }

  u.n--
}

func (u *UF) Find(p int) int {
  return u.node[p] //因为此时的树只有两层
}

第二种方式会产生层次,使树长高。显然它是利于Union而不利于Find的,因为Union操作可以一次完成,但Find操作可能需要多次访问才能找到根节点,最坏的情况就是树变成单链表。这种方式也称为quick-union算法。以下是算法的UnionFind的实现。

func (u *UF) Union(p, q int) {
  pRoot, qRoot := u.Find(p), u.Find(q)
  if pRoot == qRoot { //已连接
    return
  }
  //将连通域p合并到q
  u.node[pRoot] = qRoot

  u.n--
}

func (u *UF) Find(p int) int {
  for p != u.node[p] {
    p = u.node[p]
  }
  return p
}

quick-union算法还有一个问题,就是它"欠扁"。树的高度是影响quick-union算法性能的关键,为了避免quick-union算法中最坏情况的出现,我们需要保证每次连接时都将小树连接到大树上。为此我们需要另一个数组来记录下以每个节点为根节点的树的大小。所以这种优化算法也叫加权quick-union算法,"权"就是一颗树的节点数。

首先我们需要对数据结构做一点小小的修改:

type UF struct {
  n    int
  node []int
  size []int //记录树大小
}

func New(n int) (uf UF) {
  uf.n = n
  uf.node = make([]int, n)
  uf.size = make([]int, n)
  for i := 0; i < n; i++ {
    uf.node[i] = i
    uf.size[i] = 1 //初始时只有一个节点
  }
  return
}

下面是加权quick-union算法的Union的实现。

func (u *UF) Union(p, q int) {
  pRoot, qRoot := u.Find(p), u.Find(q)
  if pRoot == qRoot { //已连接
    return
  }
  //将连通域p合并到q
  if u.size[pRoot] < u.size[qRoot] {
    u.node[pRoot] = qRoot
    u.size[qRoot] += u.size[pRoot]
  } else {
    u.node[qRoot] = pRoot
    u.size[pRoot] += u.size[qRoot]
  }

  u.n--
}

还能不能让树再扁平一点呢?

最扁平的树是quick-find算法的树,但是Union的成本太高,没关系,一招乾坤大挪移将它的成本转嫁给Find就好了。在Find操作时,我们增加一个操作,如果当前节点的父节点不是根节点,那么就让该节点指向它的祖父节点。这样Union可以和加权quick-uion算法一样,我们将原本在Union中做的扁平化延迟到了Find中。虽然不能像quick-find一样扁,但是至少比加权quick-uion扁了不少。下面是该算法的Find的实现。

func (u *UF) Find(p int) int {
  for p != u.node[p] {
    u.node[p] = u.node[u.node[p]] //提升p节点的位置
    p = u.node[p]
  }
  return p
}

至此,连通域问题的原理和优化就已经全部介绍完了。在连通域算法中,我们只知道两个节点相连,但是不知道它们如何相连。因为我们在构造树的过程中丢掉了"如何相连"的信息,这也导致了连接无法删除。

有关连通分量(quick-union)的更多相关文章

  1. sql - rails union hack,如何将两个不同的查询放在一起 - 2

    我有一个查询,它在同一个表中搜索两个单独的字段...寻找最有可能是特定城市但也可能是国家的位置...即需要两个字段。表格看起来像:CountryCityGermanyAachenUSAAmarilloUSAAustin结果:KeywordSideinfoAachenGermanyUSACountryAustinUSAGermanyCountry基本上我想知道是否有更简洁的方法来执行此操作,因为我必须使用两个单独的查询,然后将它们加在一起,对它们进行排序等(效果很好):defself.ajax(search)countries=Location.find(:all,:select=>'c

  2. IGH主站通信测试csp模式(DC同步 preemrt)连通一从站并实现控制 - 2

    IGH主站通信测试linuxcnc配置基础机器人控制LinuxCNC与EtherCAT介绍&&PDO&SDO,搭建环境步骤需要配置IGH主站的查看这篇文章linux系统学习笔记7——一次性安装igh-ethercat主站CSP模式DC同步方式preemrt实时补丁直接上代码,这部分是直接控制使用csp模式控制一个从站运动使能后直接运动,10s,每秒607a(目标位置)增加100.注意:急停按下ESC代码分为两部分,一个是通信线程主要负责和伺服通信,使能伺服,读取和写入寄存器值。第二个是操作线程,负责修改位置的值,和监控按键。使用此代码,首先根据手册1.修改PDO条目,要和自己的伺服一致2.修改

  3. ruby-on-rails - Rails 中的命名空间模型 : What's the state of the union? - 2

    从一开始,Rails就存在命名空间模型的问题。随着时间的推移,几乎每个人都放弃了使用它。包括我自己。随着Rails2.3的发布,我想了解最新情况。我想到的具体问题是:首先,可以出发了吗?表的命名,有什么规律可循?协会,如何以最不冗长的方式声明它们?如何命名外键列?自动请求,如果将模型文件放在与命名空间匹配的子目录中,它会起作用吗?或者,如何命名和放置文件?代,模型生成器是否成功并正确地处理命名空间?生成器,包含Controller的脚手架生成器怎么样?任何应该注意的不兼容性/怪癖? 最佳答案 我见过的关于这个问题的最好的文章来自St

  4. javascript - 我如何在 Nodejs Buffer 上像 struct union type 一样处理 C? - 2

    我正在尝试在Nodejs上解析一个使用结构联合类型的缓冲区,我该如何在Nodejs上本地处理这个问题?我完全迷路了。typedefunion{unsignedintvalue;struct{unsignedintseconds:6;unsignedintminutes:6;unsignedinthours:5;unsignedintdays:15;//from01/01/2000}info;}__attribute__((__packed__))datetime; 最佳答案 这个联合要么是一个32位整数value,要么是info结构

  5. javascript - 查找无向图的所有连通分量 - 2

    我有一个对象列表(无向边),如下所示:pairs=[pair:["a2","a5"],pair:["a3","a6"],pair:["a4","a5"],pair:["a7","a9"]];我需要在单独的组中找到所有组件(连接的节点)。所以从给定的对中我需要得到:groups=[group1:["a2","a5","a4"],group2:["a3","a6"],group3:["a7","a9"]];我实际上在这里阅读了一些答案并用谷歌搜索了这个,这就是我如何了解到这被称为“在图中查找连接的组件”,但是找不到任何示例代码。我在Node.js上使用JavaScript,但任何其他语言的

  6. javascript - flatiron.js 使用 union、director 和 plates 进行路由和模板化? - 2

    来自express.js,想给flatiron尝试一个小项目。但是,有一些小问题使我无法真正取得进展。varflatiron=require('flatiron'),session=require('connect').session,ecstatic=require('ecstatic'),path=require('path'),fs=require('fs'),plates=require('plates'),director=require('director'),winston=require('winston'),union=require('union');varrout

  7. c - 如何将 Go 绑定(bind)建模为使用 union 的 C 结构? - 2

    我目前正在写一个Gowrapper对于libfreefare.libfreefare的API包含以下功能:structmifare_desfire_file_settings{uint8_tfile_type;uint8_tcommunication_settings;uint16_taccess_rights;union{struct{uint32_tfile_size;}standard_file;struct{int32_tlower_limit;int32_tupper_limit;int32_tlimited_credit_value;uint8_tlimited_credi

  8. XML 规范 : Union of xs:date, xs :gYearMonth, xs :gYear? - 2

    我正在使用XML规范,它定义了一个“类型”“日期”,即:date:Aunionofxs:date,xs:gYearMonth,xs:gYear以上数据类型来自W3CXMLSchemaDefinitionLanguage(XSD)1.1Part2我的问题是,这是否意味着我可以期待date值可能是:那些W3C类型的逗号分隔实例?(例如)或者,更简单地说,该日期的值可以是这些格式中的任何一种?或者,1.或2.?从本质上讲,我正在寻找有关“Aunionof”在这种情况下的含义的更多说明。 最佳答案 这里的“联合”是集合理论意义上的。因此,任

  9. ruby-on-rails - Rails RESTful to_xml - 如何实现连通性? - 2

    在阅读了LeonardRichardson和SamRuby合着的RESTfulWebServices一书后,在我看来,rails的to_xml方法并不像它应该的那样安静。具体来说,本书介绍了面向资源的体系结构,其原则之一是连通性:资源的表示不仅应包含资源的数据,还应包含与其他资源的链接。但是,当Rails构建资源时,它会通过推迟到[model]#to_xml来实现对xml表示的请求。此方法无法访问普通路径助手,因此指向其他资源的任何链接仅由它们的ID指示,而不是由它们的URI指示。我现在已经解决了这个问题,但解决方案似乎不是很可靠:给定一个具有嵌套雇员的雇主资源,以下代码(某种程度上)

  10. xml - 是否可以使用 xs :union for complexTypes? - 2

    这就是它应该的样子。任务是从Person派生一个Student,然后可以使用元素Kunde的两种类型之一。这似乎是无效的。 最佳答案 您不能为此使用xs:union。您可以使用xs:choice,或将元素放在替换组中,这样它们中的任何一个都可以代替替换组头部的元素。 关于xml-是否可以使用xs:unionforcomplexTypes?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questio

随机推荐