做app网站的公司哪家好/百度提交网站入口
算法分析与设计之并查集
- 1.前言
- 2.并查集的基础
- 2.1.关于动态连通性
- 2.2.动态连通性的应用场景:
- 2.3.对问题建模:
- 2.4.建模思路:
- 2.5.API
- 2.7.Quick-Find算法:
- 2.8.Quick-Union算法:
- 3. 并查集的应用
1.前言
本文主要介绍解决动态连通性一类问题的一种算法,使用到了一种叫做并查集的数据结构,称为Union-Find。
更多的信息可以参考Algorithms一书的Section 1.5,实际上本文也就是基于它的一篇读后感吧。
原文中更多的是给出一些结论,我尝试给出一些思路上的过程,即为什么要使用这个方法,而不是别的什么方法。我觉得这个可能更加有意义一些,相比于记下一些结论。
2.并查集的基础
2.1.关于动态连通性
我们看一张图来了解一下什么是动态连通性:
假设我们输入了一组整数对,即上图中的(4, 3) (3, 8)等等,每对整数代表这两个points/sites是连通的。那么随着数据的不断输入,整个图的连通性也会发生变化,从上图中可以很清晰的发现这一点。同时,对于已经处于连通状态的points/sites,直接忽略,比如上图中的(8, 9)。
2.2.动态连通性的应用场景:
- 网络连接判断:
如果每个pair中的两个整数分别代表一个网络节点,那么该pair就是用来表示这两个节点是需要连通的。那么为所有的pairs建立了动态连通图后,就能够尽可能少的减少布线的需要,因为已经连通的两个节点会被直接忽略掉。
- 变量名等同性(类似于指针的概念):
在程序中,可以声明多个引用来指向同一对象,这个时候就可以通过为程序中声明的引用和实际对象建立动态连通图来判断哪些引用实际上是指向同一对象。
2.3.对问题建模:
在对问题进行建模的时候,我们应该尽量想清楚需要解决的问题是什么。因为模型中选择的数据结构和算法显然会根据问题的不同而不同,就动态连通性这个场景而言,我们需要解决的问题可能是:
-
给出两个节点,判断它们是否连通,如果连通,不需要给出具体的路径
-
给出两个节点,判断它们是否连通,如果连通,需要给出具体的路径
就上面两种问题而言,虽然只有是否能够给出具体路径的区别,但是这个区别导致了选择算法的不同,本文主要介绍的是第一种情况,即不需要给出具体路径的Union-Find算法。
而第二种情况可以使用基于DFS的算法。
2.4.建模思路:
最简单而直观的假设是,对于连通的所有节点,我们可以认为它们属于一个组,因此不连通的节点必然就属于不同的组。随着Pair的输入,我们需要首先判断输入的两个节点是否连通。如何判断呢?按照上面的假设,我们可以通过判断它们属于的组,然后看看这两个组是否相同,如果相同,那么这两个节点连通,反之不连通。为简单起见,我们将所有的节点以整数表示,即对N个节点使用0到N-1的整数表示。而在处理输入的Pair之前,每个节点必然都是孤立的,即他们分属于不同的组,可以使用数组来表示这一层关系,数组的index是节点的整数表示,而相应的值就是该节点的组号了。该数组可以初始化为:
for(int i = 0; i < size; i++)id[i] = i;
即对于节点i,它的组号也是i。
初始化完毕之后,对该动态连通图有几种可能的操作:
- 查询节点属于的组
数组对应位置的值即为组号
- 判断两个节点是否属于同一个组
分别得到两个节点的组号,然后判断组号是否相等
- 连接两个节点,使之属于同一个组
分别得到两个节点的组号,组号相同时操作结束,不同时,将其中的一个节点的组号换成另一个节点的组号
- 获取组的数目
初始化为节点的数目,然后每次成功连接两个节点之后,递减1
2.5.API
我们可以设计相应的API:
注意其中使用整数来表示节点,如果需要使用其他的数据类型表示节点,比如使用字符串,那么可以用哈希表来进行映射,即将String映射成这里需要的Integer类型。
分析以上的API,方法connected和union都依赖于find,connected对两个参数调用两次find方法,而union在真正执行union之前也需要判断是否连通,这又是两次调用find方法。因此我们需要把find方法的实现设计的尽可能的高效。所以就有了下面的Quick-Find实现。
2.7.Quick-Find算法:
public class UF
{private int[] id; // access to component id (site indexed) 即访问组件id(下标索引)private int count; // number of components 组数public UF(int N){// Initialize component id array.初始化数组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);// 如果两个组号相等,直接返回if (pID == qID) return;// 遍历一次,改变组号使他们属于一个组for (int i = 0; i < id.length; i++)if (id[i] == pID) id[i] = qID;count--;}
}
举个例子,比如输入的Pair是(5,9),那么首先通过find方法发现它们的组号并不相同,然后在union的时候通过一次遍历,将组号1都改成8。
当然,由8改成1也是可以的,保证操作时都使用一种规则就行。
上述代码的find方法十分高效,因为仅仅需要一次数组读取操作就能够找到该节点的组号。
但是问题随之而来,对于需要添加新路径的情况,就涉及到对于组号的修改,因为并不能确定哪些节点的组号需要被修改,因此就必须对整个数组进行遍历,找到需要修改的节点,逐一修改,这一下每次添加新路径带来的复杂度就是线性关系了,如果要添加的新路径的数量是M,节点数量是N,那么最后的时间复杂度就是MN,显然是一个平方阶的复杂度,对于大规模的数据而言,平方阶的算法是存在问题的,这种情况下,每次添加新路径就是“牵一发而动全身”,想要解决这个问题,关键就是要提高union方法的效率,让它不再需要遍历整个数组。
2.8.Quick-Union算法:
考虑一下,为什么以上的解法会造成“牵一发而动全身”?
因为每个节点所属的组号都是单独记录,各自为政的,没有将它们以更好的方式组织起来,当涉及到修改的时候,除了逐一通知、修改,别无他法。所以现在的问题就变成了,如何将节点以更好的方式组织起来,组织的方式有很多种,但是最直观的还是将组号相同的节点组织在一起,想想所学的数据结构,什么样子的数据结构能够将一些节点给组织起来?
常见的就是链表,图,树,什么的了。但是哪种结构对于查找和修改的效率最高?
毫无疑问是树,因此考虑如何将节点和组的关系以树的形式表现出来。
如果不改变底层数据结构,即不改变使用数组的表示方法的话。可以采用parent-link的方式将节点组织起来,举例而言,id[p]的值就是p节点的父节点的序号,如果p是树根的话,id[p]的值就是p,因此最后经过若干次查找,一个节点总是能够找到它的根节点,即满足id[root] = root的节点,也就是组的根节点了,然后就可以使用根节点的序号来表示组号。所以在处理一个pair的时候,将首先找到pair中每一个节点的组号(即它们所在树的根节点的序号),如果属于不同的组的话,就将其中一个根节点的父节点设置为另外一个根节点,相当于将一颗独立的树编程另一颗独立的树的子树。直观的过程如下图所示。但是这个时候又引入了问题。
在实现上,和之前的Quick-Find只有find和union两个方法有所不同:
private int find(int p)
{ // 寻找p节点所在组的根节点,根节点具有性质id[root] = rootwhile (p != id[p]) p = id[p];return p;
}
public void union(int p, int q)
{ // Give p and q the same root.int pRoot = find(p);int qRoot = find(q);if (pRoot == qRoot) return;id[pRoot] = qRoot; // 将一颗树(即一个组)变成另外一课树(即一个组)的子树count--;
}
树这种数据结构容易出现极端情况,因为在建树的过程中,树的最终形态严重依赖于输入数据本身的性质,比如数据是否排序,是否随机分布等等。比如在输入数据是有序的情况下,构造的BST会退化成一个链表。在我们这个问题中,也是会出现的极端情况的,如下图所示。
为了克服这个问题,BST可以演变成为红黑树或者AVL树等等。
然而,在我们考虑的这个应用场景中,每对节点之间是不具备可比性的。因此需要想其它的办法。在没有什么思路的时候,多看看相应的代码可能会有一些启发,考虑一下Quick-Union算法中的union方法实现:
public void union(int p, int q)
{ // Give p and q the same root.int pRoot = find(p);int qRoot = find(q);if (pRoot == qRoot) return;id[pRoot] = qRoot; // 将一颗树(即一个组)变成另外一课树(即一个组)的子树count--;
}
所以我们应该考虑树的大小,然后再来决定到底是调用:
id[pRoot] = qRoot或者是id[qRoot] = pRoot
即总是size小的树作为子树和size大的树进行合并。这样就能够尽量的保持整棵树的平衡。
所以现在的问题就变成了:树的大小该如何确定?
我们回到最初的情形,即每个节点最一开始都是属于一个独立的组,通过下面的代码进行初始化:
for (int i = 0; i < N; i++)id[i] = i; // 每个节点的组号就是该节点的序号
以此类推,在初始情况下,每个组的大小都是1,因为只含有一个节点,所以我们可以使用额外的一个数组来维护每个组的大小,对该数组的初始化也很直观:
for (int i = 0; i < N; i++)sz[i] = 1; // 初始情况下,每个组的大小都是1
而在进行合并的时候,会首先判断待合并的两棵树的大小,然后按照上面图中的思想进行合并,实现代码:
public void union(int p, int q)
{int i = find(p);int j = find(q);if (i == j) return;// 将小树作为大树的子树if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }else { id[j] = i; sz[i] += sz[j]; }count--;
}
Quick-Union和Weighted Quick-Union的比较:
可以发现,通过sz数组决定如何对两棵树进行合并之后,最后得到的树的高度大幅度减小了。这是十分有意义的,因为在Quick-Union算法中的任何操作,都不可避免的需要调用find方法,而该方法的执行效率依赖于树的高度。树的高度减小了,find方法的效率就增加了,从而也就增加了整个Quick-Union算法的效率。
上图其实还可以给我们一些启示,即对于Quick-Union算法而言,节点组织的理想情况应该是一颗十分扁平的树,所有的孩子节点应该都在height为1的地方,即所有的孩子都直接连接到根节点。这样的组织结构能够保证find操作的最高效率。
那么如何构造这种理想结构呢?
在find方法的执行过程中,不是需要进行一个while循环找到根节点嘛?如果保存所有路过的中间节点到一个数组中,然后在while循环结束之后,将这些中间节点的父节点指向根节点,不就行了么?但是这个方法也有问题,因为find操作的频繁性,会造成频繁生成中间节点数组,相应的分配销毁的时间自然就上升了。那么有没有更好的方法呢?还是有的,即将节点的父节点指向该节点的爷爷节点,这一点很巧妙,十分方便且有效,相当于在寻找根节点的同时,对路径进行了压缩,使整个树结构扁平化。相应的实现如下,实际上只需要添加一行代码:
private int find(int p)
{while (p != id[p]){// 将p节点的父节点设置为它的爷爷节点id[p] = id[id[p]];p = id[p];}return p;
}
至此,动态连通性相关的Union-Find算法基本上就介绍完了,从容易想到的Quick-Find到相对复杂但是更加高效的Quick-Union,然后到对Quick-Union的几项改进,让我们的算法的效率不断的提高。
这几种算法的时间复杂度如下所示:
Algorithm | Constructor | Union | Find |
---|---|---|---|
Quick-Find | N | N | 1 |
Quick-Union | N | Tree height | Tree height |
Weighted Quick-Union | N | lgN | lgN |
Weighted Quick-Union With Path Compression | N | Very near to 1 (amortized) | Very near to 1 (amortized) |
对大规模数据进行处理,使用平方阶的算法是不合适的,比如简单直观的Quick-Find算法,通过发现问题的更多特点,找到合适的数据结构,然后有针对性的进行改进,得到了Quick-Union算法及其多种改进算法,最终使得算法的复杂度降低到了近乎线性复杂度。
如果需要的功能不仅仅是检测两个节点是否连通,还需要在连通时得到具体的路径,那么就需要用到别的算法了,比如DFS或者BFS。
3. 并查集的应用
后续就是在并查集的应用上。材料主要是取自POJ,HDOJ上的一些算法练习题。
首先还是回顾和总结一下关于并查集的几个关键点:
- 以树作为节点的组织结构,结构的形态很是否采取优化策略有很大关系,未进行优化的树结构可能会是“畸形”树(严重不平衡,头重脚轻,退化成链表等),按尺寸(正规说法叫做秩,后文全部用秩来表示)进行平衡,同时辅以路径压缩后,树结构会高度扁平化。
- 虽然组织结构比较复杂,数据表示方式却十分简洁,主要采用数组作为其底层数据结构。一般会使用两个数组(parent-link array and size array),分别用来保存当前节点的父亲节点以及当前节点所代表子树的秩。第一个数组(parent-link array)无论是否优化,都需要使用,而第二个数组(size array),在不需要按秩合并优化或者不需要保存子树的秩时,可以不使用。根据应用的不同,可能需要第三个数组来保存其它相关信息,比如HDU-3635中提到的“转移次数”。
- 主要操作包括两部分,union以及find。union负责对两颗树进行合并,合并的过程中可以根据具体应用的性质选择是否按秩优化。需要注意的是,执行合并操作之前,需要检查待合并的两个节点是否已经存在于同一颗树中,如果两个节点已经在一棵树中了,就没有合并的必要了。这是通过比较两个节点所在树的根节点来实现的,而寻找根节点的功能,自然是由find来完成了。find通过parent-link数组中的信息来找到指定节点的根节点,同样地,也可以根据应用的具体特征,选择是否采用路径压缩这一优化手段。然而在需要保存每个节点代表子树的秩的时候,则无法采用路径压缩,因为这样会破坏掉非根节点的尺寸信息(注意这里的“每个”,一般而言,在按秩合并的时候,需要的信息仅仅是根节点的秩,这时,路径压缩并无影响,路径压缩影响的只是非根节点的秩信息)。
以上就是我认为并查集中存在的几个关键点。
言归正传,来看几个利用并查集来解决问题的例子:
(说明:除了第一个问题贴了完整的代码,后面的问题都只会贴出关键部分的代码)
HDU-1213 How many tables
问题的描述是这样的:
Today is Ignatius’ birthday. He invites a lot of friends. Now it’s dinner time. Ignatius wants to know how many tables he needs at least. You have to notice that not all the friends know each other, and all the friends do not want to stay with strangers. One important rule for this problem is that if I tell you A knows B, and B knows C, that means A, B, C know each other, so they can stay in one table. For example: If I tell you A knows B, B knows C, and D knows E, so A, B, C can stay in one table, and D, E have to stay in the other one. So Ignatius needs 2 tables at least.
对这个问题抽象之后,就是要求进行若干次union操作之后,还会剩下多少颗树(或者说还剩下多少Connected Components)。反映到这个例子中,就是要求有多少“圈子”。
其实,这也是社交网络中的最基本的功能,每次系统向你推荐的那些好友一般而言,会跟你在一个“圈子”里面,换言之,也就是你可能认识的人,以并查集的视角来看这层关系,就是你们挂在同一颗树上。
给出实现代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;public class Main {public static void main(String[] args) throws NumberFormatException,IOException {BufferedReader br = new BufferedReader(new InputStreamReader(System.in));PrintWriter out = new PrintWriter(System.out);int totalCases = Integer.parseInt(br.readLine());WeightedQUWithPathCompression uf;String[] parts;while (totalCases > 0) {parts = br.readLine().split(" ");// based on 1, not 0uf = new WeightedQUWithPathCompression(Integer.parseInt(parts[0]) + 1);// construct the ufint tuples = Integer.parseInt(parts[1]);while (tuples > 0) {parts = br.readLine().split(" ");uf.union(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));tuples--;}out.println(uf.count() - 1);br.readLine();totalCases--;}out.flush();}
}class WeightedQUWithPathCompression { private int count;private int[] id;private int[] size;public WeightedQUWithPathCompression(int N) {this.count = N;this.id = new int[N];this.size = new int[N];for (int i = 0; i < this.count; i++) {id[i] = i;size[i] = 1;}}private int find(int p) {while (p != id[p]) {id[p] = id[id[p]]; // 路径压缩,会破坏掉当前节点的父节点的尺寸信息,因为压缩后,当前节点的父节点已经变了p = id[p];}return p;}public void union(int p, int q) {int pCom = this.find(p);int qCom = this.find(q);if (pCom == qCom) {return;}// 按秩进行合并if (size[pCom] > size[qCom]) {id[qCom] = pCom;size[pCom] += size[qCom];} else {id[pCom] = qCom;size[qCom] += size[pCom];}// 每次合并之后,树的数量减1count--;}public int count() {return this.count;}
}
最后,通过调用count方法获取的返回值就是树的数量,也就是“圈子”的数量。
根据问题的具体特性,上面同时采用了两种优化策略,即按秩合并以及路径压缩。因为问题本身对合并的先后关系以及子树的秩这类信息不敏感。然而,并不是所有的问题都这样,比如下面这一道题目,他对合并的先后顺序就有要求:
HDU-3635 Dragon Balls:
http://acm.hdu.edu.cn/showproblem.php?pid=3635
(上面题意的描述摘自:http://www.cnblogs.com/Shirlies/archive/2012/03/06/2382118.html)
在这道题中,对子树进行合并时,就不能按秩进行合并,因为合并是有先后关系的。
我们重点关注一下要回答的问题是什么,比如Q A代表的问题就是:
A球在哪里? — 这个问题好回答,A球所在的城市就是该子树的根节点,即find方法的返回值。
A球所在的城市有多少个球? — 同样地,这个问题的答案就是size数组中对应位置的信息,虽然本题不能按秩进行合并优化,但是秩还是需要被保存下来的。
A球被转移了多少次呢? — 这个问题画张图,就比较好理解了:
首先将球1所在城市的所有球转移到球2所在的城市中,即城市2,然后将球1所在城市的所有球转移到球3所在的城市中,即城市3。显然,在第二步中,1球已经不在城市1中,因为其在第一步中已经转移到城市2了。然后第二步实际就是将城市2中的所有球(包括球1和球2)都转移到城市3中。
紧接着,将1球所在城市的球全部转移(包括球1,2,3)到球4所在的城市中,即是将3和4进行合并。这个时候如果直接进行合并的话,会得到一个链表状的结构,这种结构使我们一直都力求避免的,所以可以采用前面使用的路径压缩进行优化。路径压缩的具体做法就不赘述了。现在需要考虑的是,经过这3轮合并,球1到底移动了多少次?如果从最后的结果图来看,球1最后到城市4,应该移动了2次,即1->3, 3->4。但是,仔细想想就会发现,这是不正确的。因为在T1 2中球1首先移动到了城市2,然后T 1 3,表示1球所在的城市中的所有球被移动到了城市3中,即城市2中的球移动到城市3中,这会对1球进行一次移动。以此类推,最后在T 1 4中,1球从城市3中移动到了城市4中,又发生了一次移动,因此,1球一共移动了3次,1->2, 2->3, 3->4。那么这就存在问题了,至少在最后的图中,这一点很不直观,因为从1到4的路径上,已经没有2的踪迹了。显然,这是路径压缩带来的副作用。因为采用了路径压缩,所以对树结构造成了一些破坏,具体而言,是能够推导出球的转移次数的信息被破坏了。试想一下,如果没有进行路径压缩,转移次数实际上是很直观的,从待求节点到根节点走过的路径数,就是转移次数。
所以为了解决引入路径压缩带来的问题,需要引入第三个数组来保存每个球的转移次数。结合题意,每次在进行转移的时候,是转移该球所在城市中所有的球到目标球所在的城市,把这句话抽象一下,就是只有根节点才能够进行合并。因此,现有的union方法还是适用的,因为它在进行真正的合并之前,还是需要首先找到两个待合并节点的根节点。然后合并的时候,将第一个球所在城市的的号码的转移次数加1。按照这种想法,实现代码为:
private static void union(int p, int q) {int pRoot = find(p);int qRoot = find(q);if (pRoot == qRoot) {return;}// 不能进行按秩合并,且在合并时,对第一个球的转移次数进行递增id[pRoot] = qRoot;trans[pRoot]++;size[qRoot] += size[pRoot];}
但是跟踪一下以上代码的调用过程不难发现,最后的球1,2,3,4的转移次数分别为1,1,1,0(唯一对trans数组进行影响的操作目前只存在于union方法中,见上)。显然,这是不正确的,正确的转移次数应该是3,2,1,0。那么是什么地方出了岔子呢,还是看看路径压缩就明白了,在路径压缩的时候,只顾着压缩,而没有对转移次数进行更新。
那么如何进行更新呢?看看上图,1本来是2的孩子,现在却成了3的孩子,跳过了2,因此可以看成,1->2->3的路径被压缩成了1->3,即2->3的这条路径被压缩了。被压缩在了1->3中,因此更新的操作也就有了基本的想法,我们可以讲被压缩的那条路径中的信息增加到压缩后的结果路径中,对应前面的例子,我们需要把2->3的信息给添加到1->3,用代码来表示的话,就是:
trans[1] += trans[2];
一般化后,实现代码如下所示:
private static int find(int q) {while (id[q] != id[id[q]]) { //如果q不是其所在子树的根节点的直接孩子trans[q] += trans[id[q]]; //更新trans数组,将q的父节点的转移数添加到q的转移数中id[q] = id[id[q]]; //对其父节点到其爷爷节点之间的路径进行压缩}return id[q];}
最后,如果需要获得球A的转移次数,直接获取trans[A]就OK了。
HDU-1856 More is better
这道题目的目的是想知道经过一系列的合并操作之后,查询在所有的子树中,秩的最大值是多少,简而言之,就是最大的那颗子树包含了多少个节点。
很显然,这个问题也能够同时使用两种优化策略,只不过因为要求最大秩的值,需要有一个变量来记录。那么在哪个地方来更新它是最好的呢?我们知道,在按秩进行合并的时候,需要比较两颗待合并子树的秩,因此可以顺带的将对秩的最大值的更新也放在这里进行,实现代码如下:
private static void union(int p, int q) {int pRoot = find(p);int qRoot = find(q);if (pRoot == qRoot) {return;}if (sz[pRoot] > sz[qRoot]) {id[qRoot] = pRoot;sz[pRoot] += sz[qRoot];if (sz[pRoot] > max) { // 如果合并后的树的秩比当前最大秩还要大,替换之max = sz[pRoot];}} else {id[pRoot] = qRoot;sz[qRoot] += sz[pRoot];if (sz[qRoot] > max) { // 如果合并后的树的秩比当前最大秩还要大,替换之max = sz[qRoot];}}}
这样,在完成了所有的合并操作之后,max中保存的即为所需要的信息。
HDU-1272 | HDU-1325 小希的迷宫 | Is it a tree ?
http://acm.hdu.edu.cn/showproblem.php?pid=1272
http://acm.hdu.edu.cn/showproblem.php?pid=1325
这两个问题都是判断是否合并后的结构是一棵树,即结构中应该没有环路,除此之外,还有边数和顶点数量的之间的关系,应该满足edges + 1 = nodes。
对于并查集,后者可以通过检查最后的connected components的数量是否为1来确定。
当然,两者在题目描述上还是有一定的区别,前者是无向图,后者是有向图。但是对于使用并查集来实现时,这一点的区别仅仅体现在合并过程无法按秩优化了。其实,如果能够采用路径压缩,按秩优化的效果就不那么明显了,因为每次进行查询操作的时候,会对被查询的节点进行路径压缩(参见find方法),可以说这是一种“懒优化”,或者叫做“按需优化”。而按秩合并则是一个主动优化的过程,每次进行合并的时候都会进行。而采用按秩合并优化,需要额外一个保存size信息的数组,在一些应用场景中,对size信息并不在意,因此为了实现可选的优化方法而增加空间复杂度,就有一些得不偿失了。并且,对于按秩合并以及路径压缩到底能够提高多少效率,我们目前也并不清楚,这里做个记号,以后有空了写一篇相关的文章。
扯远了,回到正题。前面提到了判断一张图是否是一颗树的两个关键点:
- 不存在环路(对于有向图,不存在环路也就意味着不存在强连通子图)
- 满足边数加一等于顶点数的规律(不考虑重边和指向自身的边)
------------------------------------------总结的分割线---------------------------------------
就目前看来,一般问题都是围绕着并查集的两个主要操作,union和find做文章,根据具体应用,增加一些信息,增加一些逻辑,例如上题中的转移次数,或者是根据问题特征选择使用合适的优化策略,按秩合并以及路径压缩。
并查集是我比较喜欢的一种数据结构和算法,很多实际问题都能够利用它给出高效而简洁的解决方案。后续还会陆续介绍一些有代表性的,同时难度也更大的题目,敬请关注。
相关文章:

算法分析与设计之并查集详解
算法分析与设计之并查集1.前言2.并查集的基础2.1.关于动态连通性2.2.动态连通性的应用场景:2.3.对问题建模:2.4.建模思路:2.5.API2.7.Quick-Find算法:2.8.Quick-Union算法:3. 并查集的应用1.前言 本文主要介绍解决动态…...

Linux - 内存性能评估
文章目录概述free 命令指定的时间段内不间断地监控内存的使用情况通过watch与free相结合动态监控内存状况vmstat命令监控内存“sar –r”命令组合小结概述 内存的管理和优化是系统性能优化的一个重要部分,内存资源的充足与否直接影响应用系统的使用性能。在进行内存…...

00后初中辍学,转行程序员后,终于找到了女朋友
大家好,这里是程序员晚枫,今天继续分享我们的读者投稿,如需投稿赚稿费的朋友,请在后台私信我:投稿。下面我们进入正文吧~ 我是一位 00 后,从初一辍学,到目前为止已有 8 年的时间了,在…...

“Vue学习注意事项:掌握核心特性,注意性能优化和第三方库的使用“
Vue是一款易学易用的JavaScript框架,它可以帮助开发者构建动态、高性能的用户界面。Vue的核心概念包括数据绑定、指令、计算属性和组件化,学习Vue需要注意以下几个点:1. 理解Vue的基本概念和用法Vue的基本概念包括模板、组件、数据绑定、计算…...

计算机网络协议详解(二)
文章目录🔥HTTP协议介绍🔥HTTP协议特点🔥HTTP协议发展和版本🔥HTTP协议中URI、URL、URN🔥HTTP协议的请求分析🔥HTTP协议的响应分析🔥MIME类型🔥HTTP协议介绍 HTTP协议介绍 什么是超…...

【CSS】CSS 复合选择器 ② ( 子元素选择器 | 交集选择器 )
文章目录一、子元素选择器1、语法说明2、代码分析3、代码示例二、交集选择器1、语法说明2、代码示例一、子元素选择器 1、语法说明 子元素选择器 可以选择 某个基础选择器 选择出的 元素组 的 直接子元素 ( 亲儿子元素 ) 中 使用基础选择器 选择 元素 ; 子元素选择器语法 : 父选…...

Java集合专题
文章目录框架体系CollectionListArrayListLinkedListVectorSetHashSetLinkedHashSetTreeSetMapHashMapHashtableLinkedHashMapTreeMapPropertiesCollections框架体系 1、集合主要分了两组(单列集合,双列集合) 2、Collection接口有两个重要的子…...

双重差分法(DID):算法策略效果评估的利器
文章目录算法评估DID原理简单实例Python实现算法评估 作为一名算法出身的人,曾长期热衷于算法本身的设计和优化。至于算法的效果评估,通常使用公开数据集做测试,然后对比当前已公开的结果,便可得到结论。 但是在实际落地过程中&…...

【pytorch】使用mixup技术扩充数据集进行训练
目录1.mixup技术简介2.pytorch实现代码,以图片分类为例1.mixup技术简介 mixup是一种数据增强技术,它可以通过将多组不同数据集的样本进行线性组合,生成新的样本,从而扩充数据集。mixup的核心原理是将两个不同的图片按照一定的比例…...

面向对象设计模式:创建型模式之单例模式
1. 单例模式,Singleton Pattern 1.1 Definition 定义 单例模式是确保类有且仅有一个实例的创建型模式,其提供了获取类唯一实例(全局指针)的方法。 单例模式类提供了一种访问其唯一的对象的方式,可以直接访问…...

IsADirectoryError: [Errno 21] Is a directory: ‘.‘
项目场景: 基于YOLOv5的室内场景识别 工具:colab 问题描述 Traceback (most recent call last): File “train.py”, line 630, in main(opt) File “train.py”, line 494, in main d torch.load(last, map_location‘cpu’)[‘opt’] File “/usr/…...

判断三角面片与空间中球体是否相交
文章目录一、问题描述二、解题思路 在做项目时遇到了一个数学问题,即,如何判断给定一个三角面片与空间中某个球体有相交部分?这个问题看似简单,实际处理起来需要一些方法和手段。一、问题描述 已知空间中球体的球心位置center&a…...

继承下的缺省参数值和访问说明符
前言 本文将介绍 C 继承体系下,函数缺省参数的绑定和函数访问说明符的绑定。这些奇怪的问题实际上不应在我们的代码中出现,但它们能帮助我们理解 C 的动态绑定和静态绑定,也能帮助我们更好的通过面试。 缺省参数值 先来看一段代码…...

Spring核心模块—— BeanFactoryPostProcessorBeanPostProcessor(后处理器)
后置处理器前言Spring的后处理器BeanFactoryPostProcessor(工厂后处理器)执行节点作用基本信息经典场景子接口——BeanDefinitiRegistryPostProcessor基本介绍用途具体原理例子——注册BeanDefinition使用Spring的BeanFactoryPostProcessor扩展点完成自定…...

产品新人如何培养产品思维?
什么是产品思维?其实很难定义,不同人有不同的定义。有的人定义为以用户为中心打磨一个完美体验的产品;有的定义为从需求调研到需求上线各个步骤需要思考的点,等等。本文想讨论的产品思维是:怎么去发现问题,…...

「兔了个兔」CSS如此之美,看我如何实现可爱兔兔LOADING页面(万字详解附源码)
💂作者简介: THUNDER王,一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计学专业大二本科在读,同时任汉硕云(广东)科技有限公司ABAP开发顾问。在学习工作中,我通常使用偏后…...

【Java】阻塞队列 BlcokingQueue 原理、与等待唤醒机制condition/await/singal的关系、多线程安全总结
在实习过程中使用阻塞队列对while sleep 轮询机制进行了改造,提升了发送接收的效率,这里做一点点总结。 自从Java 1.5之后,在java.util.concurrent包下提供了若干个阻塞队列,BlcokingQueue继承了Queue接口,是线程安全…...

【水下图像增强】Enhancing Underwater Imagery using Generative Adversarial Networks
原始题目Enhancing Underwater Imagery using Generative Adversarial Networks中文名称使用 GAN 增强水下图像发表时间2018年1月11日平台ICRA 2018来源University of Minnesota, Minneapolis MN文章链接https://arxiv.org/abs/1801.04011开源代码官方:https://gith…...

Maven专题总结—详细版
第一章 为什么使用Maven 获取jar包 使用Maven之前,自行在网络中下载jar包,效率较低。如【谷歌、百度、CSDN…】使用Maven之后,统一在一个地址下载资源jar包【阿里云镜像服务器等…】 添加jar包 使用Maven之前,将jar复制到项目工程…...

华为OD机试真题Java实现【字符串加密】真题+解题思路+代码(20222023)
字符串加密 题目 给你一串未加密的字符串str, 通过对字符串的每一个字母进行改变来实现加密, 加密方式是在每一个字母str[i]偏移特定数组元素a[i]的量, 数组a前三位已经赋值:a[0]=1,a[1]=2,a[2]=4。 当i>=3时,数组元素a[i]=a[i-1]+a[i-2]+a[i-3], 例如:原文 abcde …...

「Python 基础」函数与高阶函数
文章目录1. 函数调用函数定义函数函数的参数递归函数2. 高阶函数map/reducefiltersorted3. 函数式编程返回函数匿名函数装饰器偏函数1. 函数 函数是一种重复代码的抽象方式,Python 内建支持的一种封装; 调用函数 调用一个函数,需要知道函数…...

DIV内容滚动,文字符滚动标签marquee兼容稳定不卡
marquee(文字滚动)标签 marquee简介 <marquee>标签,是成对出现的标签,首标签<marquee>和尾标签</marquee>之间的内容就是滚动内容。 <marquee>标签的属性主要有behavior、bgcolor、direction、width、height、hspace、vspace、loop、scrollamount、scr…...

SpringBoot_第五章(Web和原理分析)
目录 1:静态资源 1.1:静态资源访问 1.2:静态资源源码解析-到WebMvcAutoConfiguration 2:Rest请求绑定(设置put和delete) 2.1:代码实例 2.2:源码分析到-WebMvcAutoConfiguratio…...

4-2 Linux进程和内存概念
文章目录前言进程状态进程优先级内存模型进程内存关系前言 进程是一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。一般来说,Linux系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有代码的一份拷贝。 进程状态…...

【微信小程序】计算器案例
🏆今日学习目标:第二十一期——计算器案例 ✨个人主页:颜颜yan_的个人主页 ⏰预计时间:30分钟 🎉专栏系列:我的第一个微信小程序 计算器前言实现效果实现步骤wxmlwxssjs数字按钮事件处理函数计算按钮处理事…...

408 计算机基础复试笔记 —— 更新中
计算机组成原理 计算机系统概述 问题一、冯诺依曼机基本思想 存储程序:程序和数据都存储在同一个内存中,计算机可以根据指令集执行存储在内存中的程序。这使得程序具有高度灵活性和可重用性。指令流水线:将指令分成若干阶段,每…...

找出最大数-课后程序(Python程序开发案例教程-黑马程序员编著-第二章-课后作业)
实例6:找出最大数 “脑力大乱斗”休闲益智游戏的关卡中,有一个题目是找出最大数。本实例要求编写程序,实现从输入的任意三个数中找出最大数的功能。 实例分析 对于3个数比较大小,我们可以首先先对两个数的大小进行比较ÿ…...

Java——N叉树的层序遍历
题目链接 leetcode在线oj题——N叉树的层序遍历 题目描述 给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。 树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例&…...

【Kubernetes】第十八篇 - k8s 服务发现简介
一,前言 上一篇,介绍了阿里云 ECS 服务器重启后的环境修复; 本篇,介绍 k8s 服务发现; 二,服务发现简介 当 A服务依赖了 B服务,而 B服务的IP和端口未知(或相对不固定)&…...

Codeforces Round 856 (Div. 2) 最好ak的div2
最近几场的div2 E都是一个思路啊,代码大差不差的,感觉随便ak啊。 A. Prefix and Suffix Array 题意 给你前n−1n-1n−1个字符串前缀和后n−1n-1n−1个字符串后缀,判断原字符串是否是回文串 思路 相同长度的判断是否是对称的即可。 代码 B C…...