
一、并查集
1.1 并查集
1.1.1 题意复述
有2n个人, 每两个人可组成一对情侣, 问组成n对情侣, 需要几次操作.
其中 序号为0和1为一对情侣, 序号为1和2是一对情侣, 序号为3和4是一对情侣, …, 序号为 n-2和n-1是一对情侣
1.1.2 思路
找规律, 对于每个人来说, 都有固定的序号, 即 [1…n-1]
其中第i个人, 对应的 “情侣对编号” 为 i/2
例如 第0和1个人的 “情侣对编号” 为0, 第1和2个人的情侣对编号为1, …, 第n-2和n-1个人的情侣对编号为 (n/2)-1
“情侣对编号” 可理解为一种 “集合”
那么有两种情况:
- 情侣对编号 相同的 人, 本来就已经是情侣, 则无需操作了
- 情侣对编号 不同的人, 把小集合 “合并” 为一个更大的 “集合”, 其实就是 并查集的 “并” 操作
最终返回 总 n/2 - “情侣对数” 即可
1.1.3 举例
文字并不容易描述清楚, 但结合例子则很好理解
例如, 若 [4,2,3,5,1,0,7,9,6,8] 共十个人, 问需要操作几次, 可以组成五对情侣.
首先, 得到集合数组, 因为 第 i 个人, 对应的是 i/2 对情侣, 例如 第 0和1个人对应第0对情侣, …, 第8和9个人对应第4对情侣
得到集合数组如下: [4/2,2/2,3/2,5/2,1/2,0/2,7/2,9/2,6/2,8/2] = [2,1,1,2,0,0,3,4,3,4] = [2,1, 1,2, 0,0, 3,4, 3,4]
操作前, 共有 [0], [1], [2], [3], [4] 五个集合
然后把每两个相邻的人, 所在的集合, 合并, 即并查集的合并操作, 得到 [1,2], [0], [3,4] 共三个集合
每个集合内部只需操作 n-1次, 例如 [1,2] 需操作 2-1次, [0] 需操作 1-1次, [3,4]需操作2-1次
具体的, 集合编号为[1,2], 即人员编号为 [2,1,1,2] , 则替换为 [2,2,1,1] 则只需操作1次
具体的, 集合编号为[0], 即人员编号为 [0,0] , 则无需替换, 即操作0次
具体的, 集合编号为[3,4], 即人员编号为 [3,4,3,4] , 则替换为 [3,3,4,4] 则只需操作1次
总结: 即需操作 总对数-集合数 = (n/2) - 集合数
1.1.4 编码
设计并查集
用数组即可实现, father[i] 表示第i个人的父亲的编号
并查集通常有四个方法: build(), find(i), union(a, b), isSameSet(a, b) bool
- build(): 先把每个元素, 单独建立集合, 即每个集合内只有一个元素. 即 father[i] = i
- find(i): 只要 father[i] != i, 则让 father[i] = find(father[i]), 最终返回 father[i], 即让i向上沿途所有节点的father[i]都变对(且相同)并返回
- union(a, b): 若 find(a) != find(b), 则 father[a] = b, 总集合数目 sets–
- isSameSet(a, b): return find(a) == find(b)
// go
// 每对情侣单独成为集合, 相邻的人合并成为大的集合, 最终减去集合的数量
var sets int // 集合总数
var father [31]int // 并查集数组
func minSwapsCouples(row []int) int {
n := len(row)
build(n/2)
for i := 0; i < n; i += 2 {
// 第0个人所在的集合 和 第1个人所在的集合 合并
// 第2个人所在的集合 和 第3个人所在的集合 合并
// ...
// 第n-2个人 所在的集合 和 第n-1个人所在的集合 合并
union(row[i]/2, row[i+1]/2)
}
return n/2 - sets
}
// 构建并查集
func build(m int) {
for i := range m { // i 是情侣对 的编号
father[i] = i
}
sets = m
}
// 注意, 是有修改操作的, 并不是只读操作
// 递归得到正确的 father[i], 修改, 并返回
// 操作后, i向上沿途所有节点的 father[i] 都是相同的, 都是他们集合的共同的那个代表节点
func find(i int) int {
if i != father[i] { // 注意是 if, 因为是通过递归实现的
father[i] = find(father[i])
}
return father[i]
}
// 并查集的合并
func union(a, b int) {
fa, fb := find(a), find(b)
if fa != fb {
father[fa] = fb // 注意操作的是 fa 和 fb, 即只操作了 fa和fb, 但 "fa集团的孩子们" 的 father 都还没改
sets--
}
}
// 并查集的两个元素, 所属的两个集合, 是否是同一个集合
func isSameSet(a, b int) bool {
return find(a) == find(b)
}
二、多语言解法
C p p / G o / P y t h o n / R u s t / J s / T s Cpp/Go/Python/Rust/Js/Ts Cpp/Go/Python/Rust/Js/Ts
// cpp
// go 同上
# python
// rust
// js
// ts