C++高级数据结构之并查集

目录
  • 1.动态连通性
  • 2.union-find算法API
  • 3.quick-find算法
  • 4.quick-union算法
  • 5.加权quick-union算法
  • 6.使用路径压缩的加权quick-union算法
  • 7.算法比较

前言:

  • 高级数据结构(Ⅰ)并查集(union-find)
  • 动态连通性
  • union-find算法API
  • quick-find算法
  • quick-union算法
  • 加权quick-union算法
  • 使用路径压缩的加权quick-union算法
  • 算法比较
  • 并查集 > 左神版

高级数据结构(Ⅰ)并查集(union-find)

1.动态连通性

问题的输入是一列整数对,其中每个整数都表示一个某种类型的对象,一对整数p和q可以被理解为“p和q是相连的”。我们假设“相连”是一种等价关系,这意味着它具有:

  • 自反性:p和p是相连的
  • 对称性:如果p和q是相连的,那么q和p也是相连的
  • 传递性:如果p和q是相连的且q和r是相连的,那么p和r也是相连的

本文中以下内容中使用网络方面的术语,将对象称为触点,将整数对称为连接,将等价类称为连通分量或是简称分量。简单起见,假设我们有用0到n-1整数所表示的N个触点。这样做并不会降低算法的通用性。

2.union-find算法API

为了说明问题,我们设计了一份API来封装所需的基本操作:初始化、连接两个触点、判断包含某个触点的分量、判断两个触点是否存在于同一个分量之中以及返回所有分量的数量。详细的API如下表所示

为解决动态连通性问题设计算法的任务变成了实现这份API,所有的实现都应该:

  • 定义一种数据结构表示已知的连接
  • 基于此数据结构实现高效的union()、find()、connected() 和count() 方法
  • 实现:
public class UF {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
public UF(int N) {
//初始化分量id数组
count = N;
id = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
}
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
return -1; //省略此条代码
}
public void union(int p, int q) {
//见 quick-find、qucik-union、加权uick-union
}
public static void main(String[] args) {
//解决输入的连通性问题
Scanner sc = new Scanner(System.in);
int N = sc.nextInt(); //读取触点数量
UF uf = new UF(N);
while(sc.hasNext()) {
int p = sc.nextInt();
int q = sc.nextInt(); //读取整数对
if(uf.connected(p, q)) continue; //如果已连通则忽略
System.out.println("(" + p + ", " + q + ")"); //打印连接
}
System.out.println(uf.count() + "components");
}
}

union-find的成本模型:在研究实现union-find的API的各种算法时,我们统计的是数组的访问次数(访问任意数组元素的次数,无论读写)

3.quick-find算法

此方法保证当且仅当id[p] 等于 id[q]时p和q是连通的。换句话说,在同一个连通分量中的所有触点在id[]中的值必须全部相同。这意味着connected(p, q)只需要判断id[p] == id[q],当且仅当p和q在同一连通分量之中该语句才会返回true。为了调用union(p, q)确保这一点,我们首先要检查它们是否已经存于同一个连通分量之中。如果存在于同一分量中我们不需要采取任何行动,否则我们面对的情况就是p所在的连通分量中的所有触点的id[]值均为同一个值,而q所在的连通分量中的所有触点的id[]均为另一个值。要将两个分量合二为一,我们必须将两个集合中所有触点对应的id[]元素变为同一个值。为此,我们需要遍历整个数组,将所有和id[p]相等的元素的值变为id[q]的值。我们也可以将所有和id[q]相等的元素的值变为id[p]的值,两者均可。

详细代码如下所示:

public class QuickFindUF {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
public QuickFindUF(int N) {
//初始化分量id数组
count = N;
id = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
}
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
return id[p];
}
public void union(int p, int q) {
//将p和q归并到相同的分量中
int pID = find(p);
int qID = find(q);
//如果p和q已经在相同的分量之中则不需要采取任何行动
if(pID == qID) return;
//将p的分量重命名为q的名称
for(int i = 0; i < id.length; i++) {
if(id[i] == pID) id[i] = qID;
}
count--;
}
}

输出

边(p,q) id[]
p q -|- 0 1 2 3 4 5 6 7 8 9
-------------------------------------
4 3 -|- 0 1 2 3 3 5 6 7 8 9
3 8 -|- 0 1 2 8 8 5 6 7 8 9
6 5 -|- 0 1 2 8 8 5 5 7 8 9
9 4 -|- 0 1 2 8 8 5 5 7 8 8
2 1 -|- 0 1 1 8 8 5 5 7 8 8
8 9 -|- 0 1 1 8 8 5 5 7 8 8 此时不用做任何改变,8与9已结处于同一个连通分量中
5 0 -|- 0 1 1 8 8 0 0 7 8 8
7 2 -|- 0 1 1 8 8 0 0 1 8 8
6 1 -|- 1 1 1 8 8 1 1 1 8 8
1 0 -|- 1 1 1 8 8 1 1 1 8 8 此时不用做任何改变,1与0已结处于同一个连通分量中
6 7 -|- 1 1 1 8 8 1 1 1 8 8 此时不用做任何改变,6与7已结处于同一个连通分量中

4.quick-union算法

此算法重点提高union()方法的速度,它和quick-find算法是互补的。它也基于相同的数据结构----以触点作为索引的id[]数组,但我们赋予这些值的意义不同,我们也需要用它们来定义更加复杂的结构。确切地说,每个触点对应的id[]元素都是同一个分量中另一个触点的名称(也可能是它自己)----我们将这种联系称为链接。在实现find()方法时,我们从给定的触点开始,由它的链接得到另一个触点,再由这个触点到达第三个触点,如此继续跟随着链接直到到达一个根触点,即链接指向自己的触点(这样的触点必然存在)。当且仅当分别由两个触点开始的这个过程到达了同一个根触点时它们存在于同一个连通分量中。为了保证这个过程的有效性,我们需要union(p, q)来保证这一点。它的实现很简单:我们由p和q的链接分别找到它们的根触点,然后只需将一个根触点连接到另一个根触点即可将一个分量重命名为另一个分量,因此这个算法叫做quick-union。和刚才一样,无论是重命名含有p的分量还是重命名含有q的分量都可以。

实现:

public class QuickUnionUF {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
public QuickUnionUF(int N) {
//初始化分量id数组
count = N;
id = new int[N];
for(int i = 0; i < N; i++) {
id[i] = i;
}
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
//找出分量的名称
while(p != id[p]) p = id[p];
return p;
}
public void union(int p, int q) {
//将p和q的根节点统一
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
id[pRoot] = qRoot;
count--;
}
}

输出:

5.加权quick-union算法

与其在union()中随意将一棵树连接到另一棵树,我们现在会记录每一棵树的大小并总是将较小的数接到较大的树上。这项改动需要添加一个数组和一些代码来记录树中的结点数,它能够大大改进算法的效率,提高了查询根触点的速度。该算法构造的树的高度远远小于未加权的版本所构造的树的高度。

public class WeightedQuickUnionUF {
private int[] id; //父链接数组(由触点索引)
private int[] sz; //(由触点索引的)各个根节点所对应的分量的大小
private int count; //连通分量的数量
public WeightedQuickUnionUF(int N) {
count = N;
id = new int[N];
for(int i = 0; i < N; i++) id[i] = i;
sz = new int[N];
for(int i = 0; i < N; i++) sz[i] = 1;
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public int find(int p) {
//跟随连接找到根节点
while(p != id[p]) p = id[p];
return p;
}
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
//将小树的根节点连接到大树的根节点
if(sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}else {
id[qRoot] = id[pRoot];
sz[pRoot] += sz[qRoot];
}
count--;
}
}

6.使用路径压缩的加权quick-union算法

理想情况下,我们都希望每个节点都直接链接到它的根节点上,但我们又不想像quick-union算法那样通过修改大量链接来做到这一点。我们接近这种理想状态的方式很简单,就是在检查节点的同时将他们直接链接到根节点。这种方法的实现很容易,而且这些树并没有阻止我们进行这种修改的特殊结构:如果这么做能够改进算法的效率,我们就应该实现它。要实现路径压缩,只需要为find()添加一个循环,将在路径上遇到的所有结点都直接链接到根节点。我们所得到的结果是几乎完全扁平化的树,它和quick-find算法理想情况下所得到的树非常接近。这种方法既简单又高效,但在实际情况下已经不太可能对加权quick-union算法继续进行任何改进了。

路径压缩的加权quick-union算法是最优的算法

实现:

public class PathCondenseWeightedQuickUnionUF {
private int[] id; //父链接数组(由触点索引)
private int[] sz; //(由触点索引的)各个根节点所对应的分量的大小
private int count; //连通分量的数量
public PathCondenseWeightedQuickUnionUF(int N) {
count = N;
id = new int[N];
for(int i = 0; i < N; i++) id[i] = i;
sz = new int[N];
for(int i = 0; i < N; i++) sz[i] = 1;
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
/*递归版本
public int find(int p) {
if(id[p] == p) return p;
id[p] = find(id[p]);
return id[p];
}
*/
public int find(int p) {
int root = p;
while (root != id[root]) {
root = id[root];
}
while (id[p] != root) {
int temp = p;
p = id[p];
id[temp] = root;
}
return root;
}
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot) return;
//将小树的根节点连接到大树的根节点
if(sz[pRoot] < sz[qRoot]) {
id[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}else {
id[qRoot] = id[pRoot];
sz[pRoot] += sz[qRoot];
}
count--;
}
}

输出:

7.算法比较

各种union-find算法的性能特点(存在N个触点时成本的增长数量级(最坏情况下))

到此这篇关于C高级数据结构之并查集的文章就介绍到这了,更多相关C++并查集内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++实现并查集

    本文实例为大家分享了C++实现并查集的具体代码,供大家参考,具体内容如下 #include <iostream> #include <vector> #include <cassert> using namespace std; class UnionFind{ private: vector<int> parent; int count; //优化,记录p和q所在组的深度,在合并时将深度小的结点的根指向深度大的结点的根 vector<int>

  • C++并查集常用操作

    并查集 是一种树型的数据结构,用于处理一些不相加集合的合并和查询问题.在使用中常常以森林来表示. 并查集也是用来维护集合的,和前面学习的set不同之处在于,并查集能很方便地同时维护很多集合.如果用set来维护会非常的麻烦.并查集的核心思想是记录每个结点的父亲结点是哪个结点. 前言 并查集是一种多叉树,用于处理不相交的集合的合并与查询问题(判断). 通俗理解:在日常生活中,我们会因为某个人是自己的朋友,哪怕是朋友的朋友也是有朋友,会给予通融. 偏袒.而并查集的基本概念,就是判断某两个集合是否是"朋

  • c++初级并查集知识点总结

    并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题. 有一个联合- 查找算法定义了两个用于此数据结构的操作: Find :确定元素属于哪一个子集.它可以被用来确定两个元素是否属于同一子集. Union:将两个子集合并成同一个集合. 并查集主要运用在合并元素以及查询两个元素是否在同一集合的问题,在信息学竞赛中广泛涉及 初始化: 一开始,每一个元素都是一个集合,打个比方,每个人所在的 " 家族 " 只有他一个人.我们还需要一个 f 数组,里面存的是他的 "父亲&quo

  • C++并查集亲戚(Relations)算法实例

    本文实例讲述了C++并查集亲戚(Relations)算法.分享给大家供大家参考.具体分析如下: 题目: 亲戚(Relations) 或许你并不知道,你的某个朋友是你的亲戚.他可能是你的曾祖父的外公的女婿的外甥的表姐的孙子.如果能得到完整的家谱,判断两个人是否亲戚应该是可行的,但如果两个人的最近公共祖先与他们相隔好几代,使得家谱十分庞大,那么检验亲戚关系实非人力所能及.在这种情况下,最好的帮手就是计算机. 为了将问题简化,你将得到一些亲戚关系的信息,如同Marry和Tom是亲戚,Tom和B en是

  • c++并查集优化(基于size和rank)

    基于size的优化是指:当我们在指定由谁连接谁的时候,size数组维护的是当前集合中元素的个数,让数据少的指向数据多的集合中 基于rank的优化是指:当我们在指定由谁连接谁的时候,rank数组维护的是当前集合中树的高度,让高度低的集合指向高度高的集合 运行时间是差不多的: 基于size的代码: UnionFind3.h #ifndef UNION_FIND3_H_ #define UNION_FIND3_H_ #include<iostream> #include<cassert>

  • C++并查集算法简单详解

    目录 1.并查集的初始化 2.并查集的查找操作 3.并查集的合并操作 4.为什么要路径压缩? 5.实现路径压缩 总结 1.并查集的初始化 并查集是用一个数组实现的.首先先定义一个数组: int father[N]; father[i]表示元素i的父亲结点. 接下来进行初始化.一开始,每个元素都分别是独立的一个集合,父亲结点就是它自己,所以初始化时将所有father[i]等于i: for(int i = 1; i <= N; i++){ father[i] = i; } 这样,就将father数组

  • C++利用map实现并查集

    并查集(Union-Find)是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题. 并查集存在两个操作(1.Union 联合 2.finddeputy 查找代表结点) 和一个需要解答的问题( issameset 是否 在一个集合中,或者说是否有同一个代表结点). 利用map实现主要通过两个map的对象 ,一个map<data,data>类型的fathermap,关键字为子结点,值为其父结点(父结点不一定就是代表结点),当我们需要查找两个两个元素是否在一个

  • C++高级数据结构之并查集

    目录 1.动态连通性 2.union-find算法API 3.quick-find算法 4.quick-union算法 5.加权quick-union算法 6.使用路径压缩的加权quick-union算法 7.算法比较 前言: 高级数据结构(Ⅰ)并查集(union-find) 动态连通性 union-find算法API quick-find算法 quick-union算法 加权quick-union算法 使用路径压缩的加权quick-union算法 算法比较 并查集 > 左神版 高级数据结构(Ⅰ

  • 详解Java实现数据结构之并查集

    ​一.什么是并查集 对于一种数据结构,肯定是有自己的应用场景和特性,那么并查集是处理什么问题的呢? 并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题,常常在使用中以森林来表示.在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中.其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受:即使在空

  • Java数据结构之并查集的实现

    目录 代码解析 代码应用 实际应用 并查集就是将原本不在一个集合里面的内容合并到一个集合中. 在实际的场景中用处不多. 除了出现在你需要同时去几个集合里面查询,避免出现查询很多次,从而放在一起查询的情况. 下面简单实现一个例子,我们来举例说明一下什么是并查集,以及究竟并查集解决了什么问题. 代码解析 package com.chaojilaji.book.andcheck; public class AndCheckSet { public static Integer getFather(in

  • c语言数据结构之并查集 总结

    并查集(Union-Find Set): 一种用于管理分组的数据结构.它具备两个操作:(1)查询元素a和元素b是否为同一组 (2) 将元素a和b合并为同一组. 注意:并查集不能将在同一组的元素拆分为两组. 并查集的实现: 用树来实现. 使用树形结构来表示以后,每一组都对应一棵树,然而我们就可以将这个问题转化为树的问题了,我们看两个元素是否为一组我们只要看这两个元素的根是否一致.显然,使用树形结构将问题简单化了.合并时是我们只需要将一组的根与另一组的根相连即可. 并查集的核心在于,一棵树的所有节点

  • 数据结构与算法之并查集(不相交集合)

    认识并查集 对于并查集(不相交集合),很多人会感到很陌生,没听过或者不是特别了解.实际上并查集是一种挺高效的数据结构.实现简单,只是所有元素统一遵从一个规律所以让办事情的效率高效起来. 对于定意义,百科上这么定义的: 并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中.其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受:即使在

  • java 数据结构并查集详解

    目录 一.概述 二.实现 2.1 Quick Find实现 2.2 Quick Union实现 三.优化 3.1基于size的优化 3.2基于rank优化 3.2.1路径压缩(Path Compression ) 3.2.2路径分裂(Path Spliting) 3.2.3路径减半(Path Halving) 一.概述 并查集:一种树型数据结构,用于解决一些不相交集合的合并及查询问题.例如:有n个村庄,查询2个村庄之间是否有连接的路,连接2个村庄 两大核心: 查找 (Find) : 查找元素所在

  • C#并查集(union-find)算法详解

    目录 算法的主题思想: 1. 动态连通性 2. 定义问题 3. quick-find算法实现 算法分析 4. quick-union算法实现 森林表示 算法分析 5.加权 quick-union 算法实现 算法分析 6.最优算法 - 路径压缩 算法的主题思想: 1.优秀的算法因为能够解决实际问题而变得更为重要: 2.高效算法的代码也可以很简单: 3.理解某个实现的性能特点是一个挑战: 4.在解决同一个问题的多种算法之间进行选择时,科学方法是一种重要的工具: 5.迭代式改进能够让算法的效率越来越高

  • java编程实现并查集的路径压缩代码详解

    首先看两张路径压缩的图片: 并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题.一些常见的用途有求连通子图.求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等. 使用并查集时,首先会存在一组不相交的动态集合 S={S 1 ,S 2 ,⋯,S k } ,一般都会使用一个整数表示集合中的一个元素. 每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表.每个集合中具体包含

  • python实现一个简单的并查集的示例代码

    并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题.常常在使用中以森林来表示. 并查集有三种基本操作,获得根节点,判断两节点是否连通,以及将两不连通的节点相连(相当于将两节点各自的集合合并) 用UnionFind类来表示一个并查集,在构造函数中,初始化一个数组parent,parent[i]表示的含义为,索引为i的节点,它的直接父节点为parent[i].初始化时各个节点都不相连,因此初始化parent[i]=i,让自己成为自己的父节点,从而实现各节点不互连. def __ini

随机推荐