【LeetCode】765、情侣牵手

【LeetCode】765、情侣牵手


一、并查集

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值