并查集定义

并查集是一种用来解决 连通性 的数据结构,抽象的方向不同会导致实现方式的不同。

我们也可以用并查集来表示集合的关系。

1.快速查找(quick-find)


如图所示,我们可以通过给元素加颜色来表示其所属集合,当然颜色只是一种比喻,实际上可以用数字或者其他字符来表示

这种并查集之所以能快速查找是因为查找方法的时间复杂度是O(1)

但是也有一个缺点就是合并比较慢,其合并方法的时间复杂度为O(n)

//* 1.quick-find 快速查找(合并慢)
//* 将属于同一集合的节点标记为同一颜色
class Union_quick_find {private readonly colors: number[]constructor(n: number) {this.colors = new Array(n).fill(0).map((v, i) => i)}find(index: number) {return this.colors[index]}merge(a: number, b: number) {const cb = this.colors[b]// 遍历所有元素,将属于b集合的元素归于a集合的名下for (let i = 0; i < this.colors.length; i++) {if (this.colors[i] === cb) this.colors[i] = this.colors[a]}}
}

从代码实现层面来看,我们能获得的是一个元素的所属 集合

2.快速合并 (quick-union)

顾名思义,这种并查集在合并操作时效率非常高,而查找的效率在一般的实现下是比较不稳定的,当然后面会有相应的优化方法,先介绍快速合并的并查集如何实现

在这种并查集的实现中,我们用一棵树来表示一个集合,用根节点表示一棵树(也就是一个集合)

初始化时,每个节点就是一棵树(不同树代表不同的集合)

//* 2.quick-union 快速合并
//* 将连通关系转换为树形结构,通过递归的方式快速判定
//* 老大通过树的根节点来比较
class Union_quick_union {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {if (this.boss[index] === index) return indexreturn this.find(this.boss[index])}merge(a: number, b: number) {const aBoss = this.find(a), bBoss = this.find(b)if (aBoss === bBoss) return//! 这里没有考虑到两颗树的情况,因此在极端情况下查找效率会非常低this.boss[aBoss] = bBoss}
}

从以上代码可以发现,查找方法的效率由树高和节点的位置决定,当查找的节点所在树越高、越处在底层,查找效率越慢

同时,上述代码在合并的时候也欠考虑,比如说合并的树比合并前的树高还要高,这样的结果是增加了查找的负担

那是不是就意味着矮的树合并到高的树上就行了呢?其实,我们应该根据树的平均查找次数来判断才是最公正的
树 的 平 均 查 找 次 数 = ∑ 节 点 的 查 找 次 数 节 点 数 树的平均查找次数=\frac{\sum{节点的查找次数}}{节点数} 树的平均查找次数=节点数∑节点的查找次数​

节点的查找次数表示一棵树从根节点到目标节点需要走的步数

令 有 a 、 b 两 棵 树 , a 树 有 s a 个 节 点 , 总 共 有 l a 次 查 找 次 数 b 树 有 s b 个 节 点 , 总 共 有 l b 次 查 找 次 数 以 a 为 根 的 合 并 树 的 平 均 查 找 次 数 = l a + l b + s b s a + s b 以 b 为 根 的 合 并 树 的 平 均 查 找 次 数 = l a + l b + s a s a + s b 令有a、b两棵树,a树有s_a个节点,总共有l_a次查找次数 \\ b树有s_b个节点,总共有l_b次查找次数 \\ 以a为根的合并树的平均查找次数=\frac{l_a+l_b+s_b}{s_a+s_b} \\ 以b为根的合并树的平均查找次数=\frac{l_a+l_b+s_a}{s_a+s_b} 令有a、b两棵树,a树有sa​个节点,总共有la​次查找次数b树有sb​个节点,总共有lb​次查找次数以a为根的合并树的平均查找次数=sa​+sb​la​+lb​+sb​​以b为根的合并树的平均查找次数=sa​+sb​la​+lb​+sa​​
从以上推导出来的两分式可以看出,节点数大的树的根作为合并树的根的话,合并树的查找次数越少

通俗地说就是节点少的当儿子,因此也有了以下的优化代码

//* 3.weighted-quick-union 加权快速合并
//* 通过权重考虑平均查找次数,对合并过程进行优化
//* 权重以节点数量来衡量
class Union_weighted_quick_union {private readonly boss: number[]private readonly size: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((val, i) => i)this.size = new Array(n).fill(0).map(_ => 1)}find(index: number): number {if (index === this.boss[index]) return indexreturn this.find(this.boss[index])}merge(a: number, b: number) {// 小弟没有话语权,要合并得找大哥const aBoss = this.find(a), bBoss = this.find(b)if (aBoss === bBoss) return// 小树挂在大树上if (this.size[aBoss] < this.size[bBoss]) {// a树挂在b树名下this.boss[aBoss] = bBossthis.size[bBoss] += this.size[aBoss]} else {// b树挂在a树名下this.boss[bBoss] = aBossthis.size[aBoss] += this.size[bBoss]}}
}

加权合并是合并策略的优化,查找策略也有优化方法

当我们在查找时,把遍历到的节点都直接挂在根节点下时,那以后再次查找或者查找其他节点的时候,只要经过一步就能到达根节点,这就是 路径压缩

使用路径压缩时,查找的时间复杂度接近于 O(1)

//* 4.quick-find-quick-union
//* 带路径压缩的加权平均
class Union_quick_find_quick_union {private readonly boss: number[]private readonly size: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)this.size = new Array(n).fill(0).map((v, i) => i)}// 在查找的过程中,将挂载的节点扁平化find(index: number): number {/** 递归含义:*   函数意义:把一颗树上的所有节点都挂在另一棵树的子节点上*   边界条件:当前节点为根节点时,不需要递归*   递归过程:找到要挂载树的根节点,把当前节点指向为根节点,返回根节点* */if (this.boss[index] === index) return indexconst root = this.find(this.boss[index])// 由于是递归调用,因此可以把这棵树上的所有节点都挂在老大的子节点上this.boss[index] = rootreturn root}merge(a: number, b: number) {const aBoss = this.find(a), bBoss = this.find(b)if (aBoss === bBoss) returnif (this.size[aBoss] < this.size[bBoss]) {this.boss[aBoss] = bBossthis.size[bBoss] += this.size[aBoss]} else {this.boss[bBoss] = aBossthis.size[bBoss] += this.size[aBoss]}}
}

这里的查找方法又用到了递归,就当是顺便复习一下了

在做题或比赛的时候,我们通常不会使用上述的几种比较“复杂”(其实也没那么复杂)的代码,而且也不会使用到加权合并,因为光是有路径压缩就已经极大提高效率了,两种优化方法都用相比于只有路径压缩并没有提高多少

所以就有了如下一套简洁并查集模板,在遇到具体问题时在这个基础上稍加变化即可

//* 5.只有路径压缩的并查集模板
class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {// 直接将a树挂载在b树上this.boss[this.find(a)] = this.find(b)}
}

下面的习题我们也会反复使用这个模板

总结


习题

leetCode 547 省份数量


这道题把连接的元素当成一个集合,本质上就是求集合的数量

想到集合,第一时间就想到了交并集

这道题就是交并集的一个简单应用

class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {if (this.boss[index] === index) return indexconst root = this.find(this.boss[index])this.boss[index] = rootreturn root}merge(a: number, b: number) {this.boss[this.find(a)] = this.find(b)}count() {let res = 0this.boss.forEach((v, i) => (v === i) && res++)return res}
}function findCircleNum(isConnected: number[][]): number {const len = isConnected[0]?.lengthif (!len) return 0const unionSet = new UnionSet(len)for (let i = 0; i < isConnected.length; i++) {for (let j = i + 1; j < len; j++) {if (isConnected[i][j] === 1) unionSet.merge(i, j)}}return unionSet.count()
}

leetCode 200 岛屿数量


这道题也是数集合,需要注意的是每次合并只需要判断当前位置的上面和左面(如果有的话)是否需要合并就行了,并不需要同时判断上下左右四个方向,因为该来的迟早会来的

数集合的方法就是判断当前位置元素的父节点是不是自己,如果是自己就代表当前元素就是一个树(集合)的根

class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {this.boss[this.find(a)] = this.find(b)}
}function numIslands(grid: string[][]): number {const len = grid.length * grid[0]?.lengthif (!len) return 0const rowLen = grid[0].lengthconst unionSet = new UnionSet(len)let res = 0for (let i = 0; i < grid.length; i++) {for (let j = 0; j < grid[i].length; j++) {if (grid[i][j] === '1') {// 上边if (i - 1 >= 0 && grid[i - 1][j] === '1') {unionSet.merge(i * rowLen + j, (i - 1) * rowLen + j)}// 左边if (j - 1 >= 0 && grid[i][j - 1] === '1') {unionSet.merge(i * rowLen + j, i * rowLen + j - 1)}}}}for (let i = 0; i < grid.length; i++) {for (let j = 0; j < rowLen; j++) {if (grid[i][j] === '1' && unionSet.find(i * rowLen + j) === i * rowLen + j) {res++}}}return res
}

leetCode 990 等式方程的可满足性


相等关系 => 连通性 => 并查集

首先遍历一遍给出方程,将所有相等关系放在同一集合(合并)

然后再遍历一遍方程,查找不等关系并判断不等关系的双方是否属于同一集合,如果是则冲突,不是则当前不等关系成立

class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {this.boss[this.find(a)] = this.find(b)}
}function equationsPossible(equations: string[]): boolean {const alphaBet = 'abcdefghijklmnopqrstuvwxyz'const equalSet = new UnionSet(26)equations.forEach(e => {if (e[1] === '=') {equalSet.merge(alphaBet.indexOf(e[0]), alphaBet.indexOf(e[3]))}})for (let i = 0; i < equations.length; i++) {if (equations[i][1] === '!' && equalSet.find(alphaBet.indexOf(equations[i][0])) === equalSet.find(alphaBet.indexOf(equations[i][3]))) {return false}}return true
}

leetCode 684 冗余连接


这道题也是判断集合问题,所谓冗余连接就是连接的两个元素已经在同一集合里面

所以只要遍历给定边,在连接前先判断连接的两个元素是否已经在同一集合中了,如果已经在了则表示当前连接是冗余连接,否则连接两元素

class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {this.boss[this.find(a)] = this.find(b)}
}function findRedundantConnection(edges: number[][]): number[] {const edgesSet = new UnionSet(edges.length)let res: number[] = []edges.forEach(([a, b]) => {if (edgesSet.find(a) === edgesSet.find(b)) res = [a, b]else edgesSet.merge(a, b)})return res
}

leetCode 1319 连通网络的操作次数


这道题目首先要考虑一个边界条件,就是连接数不能少于 n − 1 n-1 n−1,这个道理很简单,比如有三台计算机,要连接起来,就至少要两条网线

然后也是一个数集合的问题,把连通起来的计算机放在同一集合,最后结果就是 集 合 数 − 1 集合数-1 集合数−1

class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {this.boss[this.find(a)] = this.find(b)}count() {let res = 0this.boss.forEach((v, i) => v === i && res++)return res}
}function makeConnected(n: number, connections: number[][]): number {if (connections.length < n - 1) return -1const connectionSet = new UnionSet(n)connections.forEach(([a, b]) => {connectionSet.merge(a, b)})// 数集合的个数,连接数就是集合个数-1return connectionSet.count() - 1
}

leetCode 128 最长连续序列


这道题有两种做法,一个是先排序后计算,另一个就是并查集

先说排序的,要排序的话就要先扫描,时间复杂度高达O(nlogn),但是我们可以通过js的Set进行一定程度的优化

首先遍历输入值,将其依次加入到Set中,Set的特点是无重复,而且查找的时间复杂度是O(1) (本质上就是键名键值相同的对象)

然后再遍历这个Set,当能找到当前值-1的值是不做任何操作,直到当前值是某个序列的头,然后再依次找后续值,记录长度,最后与当前最大值比较

function longestConsecutive(nums: number[]): number {const counterSet = new Set()nums.forEach(num => counterSet.add(num))let longest = 0, current = 1for (let num of counterSet) {if (counterSet.has((num as number) - 1)) continuewhile (counterSet.has((num as number) + 1)) {(num as number) += 1current++}longest = Math.max(current, longest)current = 1}return longest
}


第二种方法就是使用并查集,所谓连续就是检验数字的连通性,我们可以将连续的数字放在同一个集合上

当然我们需要针对本题对并查集做一些相应的改进,我们需要记录每个树的大小,这个大小就是连续序列的长度

具体做法是:首先需要一个Map用于记录输入数组的值和下标的映射,因为传入数组的值可能很大,所以用下标去记录连通位置比较划算

然后就是遍历传入数组,当我们能从Map中找到当前遍历值的下一位时就连通这两个值的下标

最后返回最大集合大小即题目所求

class UnionSet {private readonly boss: number[]private readonly size: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)this.size = new Array(n).fill(0).map(_ => 1)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {const bossA = this.find(a), bossB = this.find(b)if (bossA === bossB) returnthis.boss[bossA] = bossBthis.size[bossB] += this.size[bossA]}max() {let res = 0this.size.forEach(s => res = s > res ? s : res)return res}
}function longestConsecutive(nums: number[]): number {//* 得到一个无重复的序列const counter = new Map()nums.forEach((num, index) => {if (!counter.has(num)) counter.set(num, index)})const unionSet = new UnionSet(nums.length)counter.forEach((index, num) => {if (counter.get(num + 1) !== undefined) unionSet.merge(index, counter.get(num + 1))})return unionSet.max()
}

leetCode 947 移除最多的同行或同列的石头


注意题目一个关键字:最多的

为了满足题目要求,我们可以将同行同列的石头连通成一个集合,然后拿掉 石 头 数 量 − 1 石头数量-1 石头数量−1个,也就是一个集合只留一个石头,这样我们就能拿掉尽可能多的石头

最后我们用初始石头总数量减去集合数量就是尽可能地拿走石头的数量

class UnionSet {private readonly boss: number[]public setCount: numberconstructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)this.setCount = n}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {const bossA = this.find(a), bossB = this.find(b)if (bossA === bossB) returnthis.boss[bossA] = bossBthis.setCount--}
}function removeStones(stones: number[][]): number {const stoneSet = new UnionSet(stones.length)stones.forEach(([x1, y1], i) => {for (let j = i + 1; j < stones.length; j++) {if (x1 === stones[j][0] || y1 === stones[j][1]) stoneSet.merge(i, j)}})return stones.length - stoneSet.setCount
}

leetCode 1202 交换字符串中的元素


这道题需要用并查集和优先队列(小顶堆)来解,解题思路很简单,就是实现起来比较麻烦

首先我们将连通的位置放入并查集,然后将其中的元素都放入小顶堆

最后再按照字母排序一个个吐,将其按顺序放在集合的下标中,最终结果即所求

小顶堆的数据结构如下
[string, number] <==> [字母, 下标]
映射到代码中就是 [s[index], index]

class SmallHeap {public readonly data: [string, number][]  // [字母, 下标]constructor(data?: [string, number][]) {this.data = data || []this.init()}init() {for (let i = 1; i < this.data.length; i++) this.sortUp(i)}peek() {return this.data[0]}pop() {if (!this.data.length) return nullif (this.data.length === 1) return this.data.pop()const res = this.data[0]this.data[0] = this.data.pop() as [string, number]this.sortDown(0)return res}sortUp(index: number) {let parentIndex: numberwhile (index) {parentIndex = (index - 1) >> 1if (this.data[index][0] < this.data[parentIndex][0]) this.swap(index, parentIndex)else returnindex = parentIndex}}sortDown(index: number) {let leftIndex: number, rightIndex: number, target: numberwhile (index < this.data.length) {target = indexleftIndex = (index << 1) + 1rightIndex = (index << 1) + 2if (leftIndex < this.data.length && this.data[leftIndex][0] < this.data[target][0]) target = leftIndexif (rightIndex < this.data.length && this.data[rightIndex][0] < this.data[target][0]) target = rightIndexif (index === target) returnthis.swap(index, target)index = target}}swap(i: number, j: number) {[this.data[i], this.data[j]] = [this.data[j], this.data[i]]}
}class UnionSet {public readonly data: number[]constructor(n: number) {this.data = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.data[index] = this.data[index] === index ? index : this.find(this.data[index])}merge(a: number, b: number) {this.data[this.find(a)] = this.find(b)}
}function smallestStringWithSwaps(s: string, pairs: number[][]): string {const res = new Array(s.length)const unionSet = new UnionSet(s.length)const setMap: {[key: number]: [string, number][]} = {}  // [字母, 下标]let queue: SmallHeappairs.forEach(([x, y]) => unionSet.merge(x, y))const dataSet = [...unionSet.data]  // 获得每个集合的成员分布// 按集合分类进行遍历dataSet.forEach((data, index) => {if (!setMap[unionSet.find(data)]) setMap[unionSet.find(data)] = []setMap[unionSet.find(data)].push([s[index], index])})let data: string[] = []let indexes: number[] = []let temp = 0for (let set in setMap) {// 对每个集合的字母进行排序queue = new SmallHeap(setMap[set])// 将排序好的字母复制到对应的位置上while (queue.data.length) {data.push(queue.peek()[0])indexes.push(queue.peek()[1])queue.pop()}indexes.sort((i, j) => i - j)while (indexes.length) {temp = indexes.pop() as numberres[temp] = data.pop()}}return res.join('')
}

leetCode 765 情侣牵手


观察规律可得,情侣数字右移1位得到的数字相同,因此我们创建的并查集只需要输入数组的一半(总共的情侣对数),初始化一对情侣就是一个集合

比如0和1向右位移一位得到的数字是0,因此我们把0和1称为第0对情侣,后面的以此类推

针对这道题,我们创建的并查集还需要有一个变量 setCount,该变量初始化为并查集大小,每进行一次合并操作则自减1,这个变量的含义是:不需要交换座位的情侣

题目求的是交换的次数,所以我们只要遍历传入数组,每次取两位进行并查集的合并操作,如果取的两位是属于同一对的情侣,则没有任何操作;如果取的是来自两对的情侣,则进行一次合并(不需要交换座位的情侣数-1)

class UnionSet {private readonly boss: number[]private setCount: numberconstructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)this.setCount = n}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {const bossA = this.find(a), bossB = this.find(b)if (bossA === bossB) returnthis.boss[this.find(a)] = this.find(b)this.setCount--}count() {return this.setCount  // 不需要变换座位对数}
}function minSwapsCouples(row: number[]): number {const couples = row.length >> 1 // 总对数const unionSet = new UnionSet(couples)for (let i = 0; i < row.length; i += 2) {unionSet.merge(row[i] >> 1, row[i + 1] >> 1)}return couples - unionSet.count() // 总对数 - 不需要变换座位的对数
}

leetCode 685 冗余连接II


这道题要考虑三种异常情况

  • 某个节点有两个父节点 (targetNode !== parent[targetNode])

  • 闭环 (u.find(targetNode) === u.find(parentNode))

  • 异常边的目标节点有两个父节点,而且形成闭环
    (蓝色代表边出现顺序)

联结操作如下

parent[targetNode] = parentNode
u.merge(targetNode, parentNode)

在遍历时,我们需要记录出现双父节点的边和形成闭环的边

然后就要找有问题的边,查找顺序如下

  1. 如果没有双重父节点,则直接返回形成闭环的边
  2. 如果有环路和双重父节点,则返回 [双重父节点的节点沿着形成闭环的边找到的父节点, 双重父节点]
    如以上最后一个异常情况的 [2, 1]
  3. 只有双重父节点没有闭环,则直接返回形成双重父节点的边
class UnionSet {private readonly boss: number[]constructor(n: number) {this.boss = new Array(n).fill(0).map((v, i) => i)}find(index: number): number {return this.boss[index] = this.boss[index] === index ? index : this.find(this.boss[index])}merge(a: number, b: number) {const bossA = this.find(a), bossB = this.find(b)if (bossA === bossB) returnthis.boss[bossA] = bossB}
}function findRedundantDirectedConnection(edges: number[][]): number[] {let res: number[] = []const nodeCount = edges.length// 数组从0开始,这里申请n+1的长度是为了方便计算const parent = new Array(nodeCount + 1).fill(0).map((v, i) => i)const unionSet = new UnionSet(nodeCount + 1)let twoParentsEdgeIndex = -1, cycleEdgeIndex = -1edges.forEach(([parentNode, targetNode], index) => {if (targetNode !== parent[targetNode]) {// 两个父节点twoParentsEdgeIndex = index} else if (unionSet.find(targetNode) === unionSet.find(parentNode)){// 闭环cycleEdgeIndex = index} else {// 目标节点既不会有两个父节点,也不会形成闭环parent[targetNode] = parentNodeunionSet.merge(targetNode, parentNode)}})// 找有问题的边if (twoParentsEdgeIndex === -1) {// 没有双重父节点,直接返回环路return edges[cycleEdgeIndex]} else {// 记录有双重父节点的边const conflictEdge = edges[twoParentsEdgeIndex]if (cycleEdgeIndex >= 0) {// 有环路和双重父节点return [parent[conflictEdge[1]], conflictEdge[1]]}// 只有双重父节点,没有环路return conflictEdge}
}


结语

这段时间在忙毕业论文和答辩的事情只能忙里偷闲写算法题,感觉真的很不容易

原来计划是先出来工作一段时间,然后看情况考研考个人工智能专业,但是现在写个本科论文都已经很累了,真的要考研究生的话能考上毕业也很难,感觉自己真的不适合搞学术。。。

还是脚踏实地先精通前端,然后慢慢往架构、全栈方面发展吧

算法随笔 — 树结构基础 — 并查集相关推荐

  1. POJ 1611(基础并查集)

    The Suspects 问题分析 基础并查集,就是加了个人数统计. #include <cstdio>using namespace std;const int N = 3e4+3; i ...

  2. 第一个算法项目 | JS实现并查集迷宫算法Demo学习

    0 参考链接 并查集,不就一并和一查? 520,花了一夜给女朋友写了个走迷宫游戏 点击下载并查集迷宫算法JS代码 1 JS代码修改运行方法 首先下载的文件夹如下: 双击打开maze.html文件. 键 ...

  3. 算法:程序设计之并查集

    前言 之前一直都是听说并查集,感觉是一个神乎其技,狂拽酷炫.却没有想过在自己学习并查集之前,自已在解决问题的时候也能够想到一个和并查集异曲同工的方法.这个还是很愉快的. 版权说明 著作权归作者所有. ...

  4. kruskai算法c语言实现,并查集 - xiaobaoqiu Blog

    简介 并查集是一种非常简单的数据结构,它主要涉及两个基本操作,分别为: 1. 合并两个不相交集合; 2. 判断两个元素是否属于同一个集合; 实现 实现上,通常用一个数组实现,如100个元素可以用大小为 ...

  5. 亲戚关系关系算法java程序_C++并查集亲戚(Relations)算法实例

    本文实例讲述了C++并查集亲戚(Relations)算法.分享给大家供大家参考.具体分析如下: 题目: 亲戚(Relations) 或许你并不知道,你的某个朋友是你的亲戚.他可能是你的曾祖父的外公的女 ...

  6. 算法24:LeetCode_并查集相关算法

    目录 题目一:力扣547题,求省份数量 题目二:岛屿数量 题目三:岛屿数量拓展 什么是并查集,举个简单的例子.学生考试通常会以60分为及格分数,我们将60分及以上的人归类为及格学生,而60分以下归类为 ...

  7. 并查集算法c语言版,并查集及其C程序实现.doc

    并查集及其C程序实现 等价关系与等价类 从数学上看,等价类是一个对象(或成员)的集合,在此集合中的所有对象应满足等价关系.若用符号"≡"表示集合上的等价关系,那么对于该集合中的任意 ...

  8. 【数据结构与算法】森林版并查集V1.0的Java实现

    并查集 并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中.这一类问题近几年 ...

  9. 【数据结构和算法笔记】用并查集求解等价关系

      模板: class UF { private:vector<int>father;//father数组vector<int>rank;//秩int count;//连通分支 ...

最新文章

  1. java连接mysql8
  2. 科普天地|“GPS欺骗”如何误导自动驾驶汽车
  3. linux加入windows域之完美方案
  4. CBV 验证装饰器的使用
  5. php自动加载类与路由,PHP实现路由与类自动加载步骤详解
  6. 基于Grafana的监控数据钻取功能应用实践
  7. win7发现不了无线网络_win10系统间设置共享文件夹后“网络”选项下仍然无任何文件(即发现不了共享电脑)...
  8. springboot简易集成mybatisPlus+多数据源
  9. 一直苦于没有好的资产管理软件,GLPI能解决吗?
  10. Asp.Net Core简单整理
  11. [转] GCC 中的编译器堆栈保护技术
  12. 1614. 括号的最大嵌套深度
  13. Mac 入门教程:如何更改你的 Mac 设备名称
  14. linux安装java、配置 jmeter
  15. Windows常用快捷键和Windows CMD命令大全
  16. 基于安卓Android平台的数独游戏的开发
  17. 花1分钟用Word手动绘制流程图,看完我学会了!
  18. 国瀚实业|春节投资,这些理财的坑要注意
  19. Nginx+Tomcat 搭建负载均衡、动静分离(tomcat多实例)
  20. 竞品分析报告-网易蜗牛读书

热门文章

  1. 从PD充电器取9V12V给产品供电快充,PD取电芯片概述
  2. keras教程_Keras教程
  3. PPT文件无法打开处理
  4. VelocityEngine 和Velocity类解析
  5. iOS7+系统自带条码扫描
  6. 脚踏实地做好本职工作——职场人士寓言(4)
  7. Python写入和读取Excel表格数据
  8. flutter 内网安装包生成二维码
  9. 饥荒联机版连不上服务器_《饥荒》无法连接klei服务器 刷不出服务器解决办法...
  10. 时光飞逝,思考,实践,伴我一生的经验