【重拾算法】并查集
2022/1/12 20:34:13
本文主要是介绍【重拾算法】并查集,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、什么是并查集
并查集是图论中的一种算法
集就是集合,因此可以看出并查集与集合操作有关
并查集内有两个重要的操作:合并(union),查询(find)
合并操作是用来将不同的集合合并为一个集合,查询操作用来查询某个元素所属的集合
二、举个栗子
1. 栗子
假设现在有六个元素,代号分别为1、2、3、4、5、6
他们的集合关系如下,{1, 2}, {1, 5}, {3, 4}, {6, 2},在同一花括号内表示处在同一集合中
问元素[1, 3],元素[2, 5]是否在同一集合内
思路:根据提供的集合关系,将同一集合内的元素连在一起,每个集合选取一个根节点作为集合的代表,若两个元素所处集合的根节点相同,则表示这两个元素在同一集合内
逐步建立他们的集合关系如下
初始时,每个元素都是一个集合,他们的根节点都是自己,例如:parent[1] = 1
合并元素1和元素2,此时根节点分别为1,2,任意选取元素1作为合并后集合的根节点
将元素5与元素1合并到一个集合中,根节点分别为1,5,任意选取元素5作为合并后集合的根节点
合并元素3和元素4,根节点分别为3,4,任意选取元素3作为合并后集合的根节点
合并元素2和元素6,根节点分别为5,6,任意选取元素6作为合并后集合的根节点
集合关系就建立完毕啦!
这时候开始判断元素所属集合是否相同
[1, 3]
元素1所处集合的根节点是6,元素3所处集合的根节点是3,因此他们属于不同的集合
[2, 5]
元素5所处集合的根节点是6,元素2所处集合的根节点也是6,因此他们在同一集合内
2. 检测环的存在
并查集还可以用来判断是否有环的存在
例如,我们在原有集合的基础上,再添加一条关联关系{1, 6}
元素1所在集合的根节点是6,元素6所在集合的根节点也是6,他们在同一集合中,因此若继续添加关联关系则会导致环的产生
3. 代码实现
直接用golang实现
// 初始化 func initArray(parent []int) { for i := 0; i < len(parent); i++ { parent[i] = -1 } } // 查找根节点 func find(node int, parent []int) int { if parent[node] == -1 { return node } return find(parent[node], parent) } // 合并集合 func union(x, y int, parent) bool { xRoot := find(x, parent) yRoot := find(y, parent) if xRoot == yRoot { // 根节点相同表示存在环 return false } parent[xRoot] = yRoot return true } func TestDisjointSet(t *testing.T) { elementCount, relationCount, questionCount := 6, 4, 2 var relations = [][]int{ {1, 2}, {1, 5}, {3, 4}, {6, 2}, } var P = [][]int{ {1, 3}, {2, 5}, } // 1. 初始化 parent := make([]int, elementCount+1) initArray(parent) // 2. 合并集合 for i := 0; i < relationCount; i++ { union(relations[i][0], relations[i][1], parent) } // 3. 判断是否为同一集合 for i := 0; i < questionCount; i++ { aRoot := find(P[i][0], parent) bRoot := find(P[i][1], parent) if aRoot == bRoot { t.Log(true) continue } t.Log(false) } }
执行结果
三、路径压缩
1. 路径优化
上述的步骤中,左侧的集合极端情况下需要遍历全链表,因此需要考虑用路径压缩做优化
还以上面的元素举例,初始状态仍是六个集合,要建立的关联关系如下:{1, 2}, {1, 5}, {3, 4}, {6, 2}
最初为每个集合赋予基础深度rank为0(这里初始赋值为几并不重要,它只用来做大小比较,只要每个元素的初始值相同即可)
整合元素1和元素2,根节点分别为1,2,此时他们的集合内元素的数量相等,因此可以随意指定一个根节点,这里最终选定的根节点的rank要加一,表示树深度的增加
整合元素1和元素5,根节点分别为1,5,而rank[1] > rank[5],因此选择元素1作为最终的根节点,保证树的深度尽可能的小,这也是路径压缩的目的,这里树的深度没有增加,因此rank[1]不用自增
整合元素3和元素4,根节点分别为3,4,rank[3] == rank[4],因此可以任意选择一个根节点,且rank[root]++
整合元素6和元素2,根节点分别为1,6,rank[1] > rank[6],选择让元素1作为最终的根节点
这里最终生成树的层级比上面的层级更小,遍历起来也更快,这就是路径压缩,让生成树的深度尽可能的小,减少需要遍历的节点
路径压缩的基本思想就是将深度更小的树挂载到深度更大的树上,保证最终的树深度尽可能小
2. 代码实现
// 初始化 func initArray(parent, rank []int) { for i := 0; i < len(parent); i++ { parent[i] = -1 rank[i] = 0 } } // 查找根节点 func find(node int, parent []int) int { if parent[node] == -1 { return node } return find(parent[node], parent) } // 合并集合 func union(x, y int, parent, rank []int) bool { xRoot := find(x, parent) yRoot := find(y, parent) if xRoot == yRoot { return false } if rank[xRoot] > rank[yRoot] { parent[yRoot] = xRoot } else if rank[xRoot] < rank[yRoot] { parent[xRoot] = yRoot } else { parent[xRoot] = yRoot // 这里随便选哪个节点都可以 rank[yRoot]++ } return true } func TestDisjointSet(t *testing.T) { elementCount, relationCount, questionCount := 6, 4, 2 var relations = [][]int{ {1, 2}, {1, 5}, {3, 4}, {6, 2}, } var P = [][]int{ {1, 3}, {2, 5}, } // 1. 初始化 parent := make([]int, elementCount+1) rank := make([]int, elementCount+1) initArray(parent, rank) // 2. 合并集合 for i := 0; i < relationCount; i++ { union(relations[i][0], relations[i][1], parent, rank) } // 3. 判断是否为同一集合 for i := 0; i < questionCount; i++ { aRoot := find(P[i][0], parent) bRoot := find(P[i][1], parent) if aRoot == bRoot { t.Log(true) continue } t.Log(false) } }
执行结果
这篇关于【重拾算法】并查集的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-15JavaMailSender是什么,怎么使用?-icode9专业技术文章分享
- 2024-11-15JWT 用户校验学习:从入门到实践
- 2024-11-15Nest学习:新手入门全面指南
- 2024-11-15RestfulAPI学习:新手入门指南
- 2024-11-15Server Component学习:入门教程与实践指南
- 2024-11-15动态路由入门:新手必读指南
- 2024-11-15JWT 用户校验入门:轻松掌握JWT认证基础
- 2024-11-15Nest后端开发入门指南
- 2024-11-15Nest后端开发入门教程
- 2024-11-15RestfulAPI入门:新手快速上手指南