LeetCode Go · 并查集(4) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 4 题 · 点击题目展开,先独立思考,再查看解法。
3695. 交换元素后的最大交替和 ​
题目、示例与约束
给你一个整数数组 nums。
你希望最大化 nums 的 交替和:将偶数下标的元素 相加 并 减去 奇数索引的元素获得的值。即 nums[0] - nums[1] + nums[2] - nums[3]...
同时给你一个二维整数数组 swaps,其中 swaps[i] = [pi, qi]。对于 swaps 中的每对 [pi, qi],你可以交换索引 pi 和 qi 处的元素。这些交换可以进行任意次数和任意顺序。
返回 nums 可能的最大 交替和。
示例 1:
输入:nums = [1,2,3], swaps = [[0,2],[1,2]]
输出:4
解释:
当 nums 为 [2, 1, 3] 或 [3, 1, 2] 时,可以实现最大交替和。例如,你可以通过以下方式得到 nums = [2, 1, 3]。
- 交换
nums[0]和nums[2]。此时nums为[3, 2, 1]。 - 交换
nums[1]和nums[2]。此时nums为[3, 1, 2]。 - 交换
nums[0]和nums[2]。此时nums为[2, 1, 3]。
示例 2:
输入:nums = [1,2,3], swaps = [[1,2]]
输出:2
解释:
不进行任何交换即可实现最大交替和。
示例 3:
输入:nums = [1,1000000000,1,1000000000,1,1000000000], swaps = []
输出:-2999999997
解释:
由于我们不能进行任何交换,因此不进行任何交换即可实现最大交替和。
提示:
2 <= nums.length <= 1051 <= nums[i] <= 1090 <= swaps.length <= 105swaps[i] = [pi, qi]0 <= pi < qi <= nums.length - 1[pi, qi] != [pj, qj]
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
用并查集把 和 合并。
合并后,同一集合(连通块)中的元素可以随意交换。
贪心地:
- 交替和中取负号的元素,位于奇数下标,我们把较小元素交换到奇数下标上。
- 交替和中取正号的元素,位于偶数下标,我们把较大元素交换到偶数下标上。
为此,并查集需要额外维护集合中的奇数下标个数。
对于同一组,设奇数下标个数为 。把元素从小到大排序后,前 个元素取负号,其余元素取正号。
本题视频讲解,欢迎点赞关注~
复杂度分析
- 时间复杂度: 或 ,其中 是 的长度。使用建图 + DFS + 快速选择算法,可以把时间复杂度优化到 。
- 空间复杂度:。
相似题目
- 1202. 交换字符串中的元素 1855
- 1722. 执行交换操作后的最小汉明距离 1892
专题训练
- 图论题单的「§1.1 深度优先搜索(DFS)」。
- 数据结构题单的「七、并查集」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
import "slices"
// https://space.bilibili.com/206214
// 模板来源 https://leetcode.cn/circle/discuss/mOr1u6/
type unionFind struct {
fa []int // 代表元
odd []int // 集合中的奇数个数
}
func newUnionFind(n int) unionFind {
fa := make([]int, n)
odd := make([]int, n)
// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
// 集合 i 的代表元是自己
for i := range fa {
fa[i] = i
odd[i] = i % 2
}
return unionFind{fa, odd}
}
// 返回 x 所在集合的代表元
// 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
func (u unionFind) find(x int) int {
// 如果 fa[x] == x,则表示 x 是代表元
if u.fa[x] != x {
u.fa[x] = u.find(u.fa[x]) // fa 改成代表元
}
return u.fa[x]
}
// 把 from 所在集合合并到 to 所在集合中
func (u *unionFind) merge(from, to int) {
x, y := u.find(from), u.find(to)
if x == y { // from 和 to 在同一个集合,不做合并
return
}
u.fa[x] = y // 合并集合
u.odd[y] += u.odd[x] // 更新集合中的奇数个数
}
func maxAlternatingSum(nums []int, swaps [][]int) (ans int64) {
n := len(nums)
uf := newUnionFind(n)
for _, p := range swaps {
uf.merge(p[0], p[1])
}
g := make([][]int, n)
for i, x := range nums {
f := uf.find(i)
g[f] = append(g[f], x) // 相同集合的元素分到同一组
}
for i, a := range g {
if a == nil {
continue
}
slices.Sort(a)
odd := uf.odd[i]
// 小的取负号,大的取正号
for j, x := range a {
if j < odd {
ans -= int64(x)
} else {
ans += int64(x)
}
}
}
return
}
3710. 最大划分因子 ​
题目、示例与约束
给你一个二维整数数组 points,其中 points[i] = [xi, yi] 表示笛卡尔平面上第 i 个点的坐标。
两个点 points[i] = [xi, yi] 和 points[j] = [xj, yj] 之间的 曼哈顿距离 是 |xi - xj| + |yi - yj|。
将这 n 个点分成 恰好两个非空 的组。一个划分的 划分因子 是位于同一组内的所有无序点对之间 最小 的曼哈顿距离。
返回所有有效划分中 最大 可能的 划分因子 。
注意: 大小为 1 的组不存在任何组内点对。当 n = 2 时(两个组大小都为 1),没有组内点对,划分因子为 0。
示例 1:
输入: points = [[0,0],[0,2],[2,0],[2,2]]
输出: 4
解释:
我们将点分成两组: {[0, 0], [2, 2]} 和 {[0, 2], [2, 0]}。
-
在第一组中,唯一的点对之间的曼哈顿距离是
|0 - 2| + |0 - 2| = 4。 -
在第二组中,唯一的点对之间的曼哈顿距离也是
|0 - 2| + |2 - 0| = 4。
此划分的划分因子是 min(4, 4) = 4,这是最大值。
示例 2:
输入: points = [[0,0],[0,1],[10,0]]
输出: 11
解释:
我们将点分成两组: {[0, 1], [10, 0]} 和 {[0, 0]}。
-
在第一组中,唯一的点对之间的曼哈顿距离是
|0 - 10| + |1 - 0| = 11。 -
第二组是单元素组,因此不存在任何点对。
此划分的划分因子是 11,这是最大值。
提示:
2 <= points.length <= 500points[i] = [xi, yi]-108 <= xi, yi <= 108
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
方法一:二分答案 + 判断二分图
转化
假设划分因子 。
越小,要求就越宽松,越能找到一个合法划分。
越大,要求就越苛刻,越不能找到一个合法划分。
据此,可以二分猜答案。关于二分算法的原理,请看 二分查找 红蓝染色法【基础算法精讲 04】。
现在问题变成一个判定性问题:
- 给定 ,能否把点集分成两个非空集合,使得每个集合内部的任意点对的曼哈顿距离都 ?
思路
对于曼哈顿距离 的点对(非法点对),连一条边,我们可以得到一个无向图。
我们需要把这些点分成两个集合,每个集合内部不能有边(否则违背要求)。换句话说,每条边的两个端点,一定来自不同集合。
这和 785. 判断二分图 是完全一样的,做法见【图解】交替染色法。
细节
下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的,喜欢哪种写法就用哪种。
- 开区间左端点初始值:。可以随意划分,一定满足要求。
- 开区间右端点初始值:最大曼哈顿距离加一。此时一定无法满足要求。
对于开区间写法,简单来说
check(mid) == true时更新的是谁,最后就返回谁。相比其他二分写法,开区间写法不需要思考加一减一等细节,更简单。推荐使用开区间写二分。
注意特判 的情况,此时划分因子恒为 。
答疑
问:为什么二分结束后,答案 一定来自 的某两个点的曼哈顿距离?
答:反证法。假设 不来自 的某两个点的曼哈顿距离,这意味着最小曼哈顿距离 ,也就是 。换句话说,。但根据循环不变量,二分结束后 ,矛盾。故原命题成立。
本题视频讲解,欢迎点赞关注~
复杂度分析
- 时间复杂度:,其中 是 的长度, 是曼哈顿距离的最大值。
- 空间复杂度:。
注:也可以把所有曼哈顿距离存到一个列表中,排序,在列表中二分,从而做到 的时间。
方法二:排序 + 并查集
像 Kruskal 算法那样,按照曼哈顿距离从小到大处理点对。
这些点对是互斥的,不能在同一个集合中。
这可以用带权并查集解决,见 399. 除法求值。
本题边权为 ,距离算子为模 意义下的加法,即异或运算。
复杂度分析
- 时间复杂度:,其中 是 的长度。
- 空间复杂度:。
专题训练
见下面图论题单的「七、二分图染色」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
import (
"slices"
"sort"
)
// https://space.bilibili.com/206214
// 原理见 785. 判断二分图
func isBipartite(points [][]int, low int) bool {
colors := make([]int8, len(points))
var dfs func(int, int8) bool
dfs = func(x int, c int8) bool {
colors[x] = c
p := points[x]
for y, q := range points {
if y == x || abs(p[0]-q[0])+abs(p[1]-q[1]) >= low { // 符合要求
continue
}
if colors[y] == c || colors[y] == 0 && !dfs(y, -c) {
return false // 不是二分图
}
}
return true
}
// 可能有多个连通块
for i, c := range colors {
if c == 0 && !dfs(i, 1) {
return false
}
}
return true
}
func maxPartitionFactor1(points [][]int) int {
n := len(points)
if n == 2 {
return 0
}
// 不想算的话可以写 maxDis = int(2e8)
maxDis := 0
for i, p := range points {
for _, q := range points[:i] {
maxDis = max(maxDis, abs(p[0]-q[0])+abs(p[1]-q[1]))
}
}
return sort.Search(maxDis, func(low int) bool {
// 二分最小的不满足要求的 low+1,就可以得到最大的满足要求的 low
return !isBipartite(points, low+1)
})
}
//
type unionFind struct {
fa []int
dis []int8 // dis[x] 表示 x 到其代表元的距离
}
func newUnionFind(n int) unionFind {
fa := make([]int, n)
dis := make([]int8, n)
for i := range fa {
fa[i] = i
}
return unionFind{fa, dis}
}
// 返回 x 所在集合的代表元
// 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
func (u unionFind) find(x int) int {
if u.fa[x] != x {
rt := u.find(u.fa[x])
u.dis[x] ^= u.dis[u.fa[x]] // 更新 x 到其代表元的距离
u.fa[x] = rt
}
return u.fa[x]
}
// 合并两个互斥的点
// 如果已经合并,返回是否与已知条件矛盾
func (u *unionFind) merge(from, to int) bool {
x, y := u.find(from), u.find(to)
if x == y { // from 和 to 在同一个集合,不合并
return u.dis[from] != u.dis[to] // 是否与已知信息矛盾
}
// 2 ------ 4
// / /
// 1 ------ 3
// 如果知道 1->2 的距离和 3->4 的距离,现在合并 1 和 3,并传入 1->3 的距离(本题等于 1)
// 由于 1->3->4 和 1->2->4 的距离相等
// 所以 2->4 的距离为 (1->3) + (3->4) - (1->2)
u.dis[x] = 1 ^ u.dis[to] ^ u.dis[from]
u.fa[x] = y
return true
}
func maxPartitionFactor(points [][]int) int {
n := len(points)
type tuple struct{ dis, x, y int }
manhattanTuples := make([]tuple, 0, n*(n-1)/2) // 预分配空间
for i, p := range points {
for j, q := range points[:i] {
manhattanTuples = append(manhattanTuples, tuple{abs(p[0]-q[0]) + abs(p[1]-q[1]), i, j})
}
}
slices.SortFunc(manhattanTuples, func(a, b tuple) int { return a.dis - b.dis })
uf := newUnionFind(n)
for _, t := range manhattanTuples {
if !uf.merge(t.x, t.y) {
return t.dis // t.x 和 t.y 必须在同一个集合,t.dis 就是这一划分的最小划分因子
}
}
return 0
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3873. 添加一个点后可激活的最大点数 ​
题目、示例与约束
给你一个二维整数数组 points,其中 points[i] = [xi, yi] 表示第 i 个点的坐标。points 中的所有坐标都 互不相同。
如果一个点被 激活,那么所有与该点具有相同 x 坐标或 y 坐标的点也会被 激活。
激活会一直持续,直到没有额外的点可以被激活为止。
你可以 额外添加 一个不在 points 数组中的整数坐标点 (x, y) 。从这个新添加的点开始 激活。
返回一个整数,表示可以被激活的 最大 点数,包括新添加的点。
示例 1:
输入: points = [[1,1],[1,2],[2,2]]
输出: 4
解释:
添加并激活一个点,例如 (1, 3),会导致以下激活:
(1, 3)与(1, 1)和(1, 2)具有相同的x = 1,因此(1, 1)和(1, 2)被激活。(1, 2)与(2, 2)具有相同的y = 2,因此(2, 2)被激活。
因此,被激活的点为 (1, 3), (1, 1), (1, 2), (2, 2),总计 4 个点。可以证明这是最大激活点数。
示例 2:
输入: points = [[2,2],[1,1],[3,3]]
输出: 3
解释:
添加并激活一个点,例如 (1, 2),会导致以下激活:
(1, 2)与(1, 1)具有相同的x = 1,因此(1, 1)被激活。(1, 2)与(2, 2)具有相同的y = 2,因此(2, 2)被激活。
因此,被激活的点为 (1, 2), (1, 1), (2, 2),总计 3 个点。可以证明这是最大激活点数。
示例 3:
输入: points = [[2,3],[2,2],[1,1],[4,5]]
输出: 4
解释:
添加并激活一个点,例如 (2, 1),会导致以下激活:
(2, 1)与(2, 3)和(2, 2)具有相同的x = 2,因此(2, 3)和(2, 2)被激活。(2, 1)与(1, 1)具有相同的y = 1,因此(1, 1)被激活。
因此,被激活的点为 (2, 1), (2, 3), (2, 2), (1, 1),总计 4 个点。
提示:
1 <= points.length <= 105points[i] = [xi, yi]-109 <= xi, yi <= 109points中的坐标均 互不相同。
方法一:并查集 · Go 实现
思考
激活沿相同 或 传播,可额外加一个新点,求能激活的最多点数。 。
同一 或同一 的点属于一个连通分量。并查集把坐标视为节点:点 把 与 合并。
新点可连接两个原本不同的分量。应选点数最大的两个分量并额外计新点本身。
为避免 撞号,将 平移 后再参与并查。
我们可以使用并查集来解决这个问题。
首先,我们将所有点的 坐标和 坐标进行映射,使得它们在同一个并查集中。具体来说,我们可以将 坐标加上一个足够大的常数 (例如 ),以确保 和 坐标不会发生冲突。
接下来,我们遍历所有点,将具有相同 坐标或 坐标的点进行合并。这样,具有相同 或 坐标的点就会被分到同一个集合中。
最后,我们统计每个集合中的点数,并找到最大的两个集合的大小。由于我们可以添加一个新的点来连接这两个集合,因此最终的答案就是这两个集合的大小之和再加上 1。
时间复杂度 ,其中 是点的数量,而 是阿克曼函数的反函数。空间复杂度 。
type UnionFind struct {
p map[int64]int64
size map[int64]int
}
func NewUnionFind() *UnionFind {
return &UnionFind{
p: map[int64]int64{},
size: map[int64]int{},
}
}
func (uf *UnionFind) find(x int64) int64 {
if _, ok := uf.p[x]; !ok {
uf.p[x] = x
uf.size[x] = 1
}
if uf.p[x] != x {
uf.p[x] = uf.find(uf.p[x])
}
return uf.p[x]
}
func (uf *UnionFind) union(a, b int64) bool {
pa, pb := uf.find(a), uf.find(b)
if pa == pb {
return false
}
if uf.size[pa] > uf.size[pb] {
uf.p[pb] = pa
uf.size[pa] += uf.size[pb]
} else {
uf.p[pa] = pb
uf.size[pb] += uf.size[pa]
}
return true
}
func maxActivated(points [][]int) int {
uf := NewUnionFind()
m := int64(3e9)
for _, p := range points {
uf.union(int64(p[0]), int64(p[1])+m)
}
cnt := map[int64]int{}
for _, p := range points {
root := uf.find(int64(p[0]))
cnt[root]++
}
mx1, mx2 := 0, 0
for _, x := range cnt {
if mx1 < x {
mx2 = mx1
mx1 = x
} else if mx2 < x {
mx2 = x
}
}
return mx1 + mx2 + 1
}
3910. 统计节点和为偶数的连通子图 ​
题目、示例与约束
给你一个无向图,有 n 个节点,编号从 0 到 n - 1。节点 i 的 值 为 nums[i],可以是 0 或 1。图的边由一个二维数组 edges 给出,其中 edges[i] = [ui, vi] 表示节点 ui 和节点 vi 之间的一条边。
对于图中节点的 非空子集 s,我们考虑由 s 生成的 诱导子图 如下:
- 我们只保留
s中的节点。 - 我们只保留两个端点都在
s中的边。
返回一个整数,表示图中满足以下条件的节点的 非空 子集 s 的数量:
s的 诱导子图 是 连通的。s中节点 值 的 总和 是 偶数。
示例 1:
输入: nums = [1,0,1], edges = [[0,1],[1,2]]
输出: 2
解释:
s |
是否连通? | 节点值总和 | 和是否为偶数? |
|---|---|---|---|
[0] |
是 | 1 | 否 |
[1] |
是 | 0 | 是 |
[2] |
是 | 1 | 否 |
[0,1] |
是 | 1 | 否 |
[0,2] |
否,节点 0 和节点 2 不连通。 | 2 | 否 |
[1,2] |
是 | 1 | 否 |
[0,1,2] |
是 | 2 | 是 |
示例 2:
输入: nums = [1], edges = []
输出: 0
解释:
s |
是否连通? | 节点值总和 | 和是否为偶数? |
|---|---|---|---|
[0] |
是 | 1 | 否 |
提示:
1 <= n == nums.length <= 13nums[i]是 0 或 1。0 <= edges.length <= n * (n - 1) / 2edges[i] = [ui, vi]0 <= ui < vi < n- 所有边都是 互不相同 的。
方法一:二进制枚举 + DFS · Go 实现
思考
连通诱导子图的数量在一般图上难以直接计数,但 ,全部非空子集只有 个,可以逐个检验。
先按位权和筛掉奇数和;对偶数和子集,用位掩码 标记「不在子集中」的点,再从子集中任一点 DFS,只在子集内走边。若最终 填满全部 位,则诱导子图连通。
邻接表保证 DFS 为 ,总时间 。
我们注意到,题目中节点的数量不超过 ,因此我们可以枚举节点的所有非空子集 ,对于每个子集,我们可以计算出其节点值的总和,并判断其诱导子图是否连通。
具体地,我们可以使用一个整数 来表示子集 ,其中 的第 位为 表示节点 在子集中,为 表示节点 不在子集中。对于每个子集,我们首先计算出其节点值的总和,如果总和为奇数,则跳过该子集;否则,我们使用 DFS 来判断其诱导子图是否连通。我们可以使用一个整数 来表示已经访问过的节点,初始时 的第 位为 表示节点 不在子集中,为 表示节点 在子集中。我们从子集 中的任意一个节点开始 DFS,访问其所有相邻节点,并将访问过的节点在 中标记为 。最后,如果 的所有位都为 ,则说明子集 的诱导子图是连通的,我们将答案加 。
时间复杂度 ,空间复杂度 ,其中 和 分别是节点数和边数。
func evenSumSubgraphs(nums []int, edges [][]int) int {
n := len(nums)
g := make([][]int, n)
for _, e := range edges {
g[e[0]] = append(g[e[0]], e[1])
g[e[1]] = append(g[e[1]], e[0])
}
m := (1 << n) - 1
ans := 0
var vis int
var dfs func(int)
dfs = func(u int) {
vis |= 1 << u
for _, v := range g[u] {
if (vis >> v & 1) == 0 {
dfs(v)
}
}
}
for sub := 1; sub <= m; sub++ {
s := 0
for i := 0; i < n; i++ {
if sub>>i&1 == 1 {
s += nums[i]
}
}
if s%2 != 0 {
continue
}
vis = m ^ sub
dfs(bits.Len(uint(sub)) - 1)
if vis == m {
ans++
}
}
return ans
}
继续阅读
- 实践案例
- 实践案例
- 实践案例