1. 问题背景与核心挑战
这道题目出现在LeetCode第1627题,属于图论与并查集结合的经典难题。题目要求我们处理一个带阈值的图连通性问题:给定整数n和阈值k,构建一个无向图,其中节点编号从1到n。当两个节点a和b满足gcd(a,b) > k时,它们之间存在一条边。最终需要回答多组查询,判断任意两个节点是否连通。
这个问题的核心难点在于:
- 直接暴力计算每对节点的gcd并建图,时间复杂度高达O(n²),在n=1e4时完全不可行
- 需要找到一种数学上的优化规律,避免显式存储所有边
- 并查集的高效实现与路径压缩技巧成为解题关键
2. 数学优化思路:从因数角度重建连接
2.1 最大公约数的性质分析
gcd(a,b) > k意味着a和b至少共享一个大于k的质因数。这个数学性质让我们可以转换思路:不再关注节点之间的直接连接,而是通过它们的公共因数来建立间接连接。
具体来说:
- 对于每个数x∈[1,n],找出其所有大于k的因数
- 如果多个数共享同一个因数d>k,那么它们都应该被连通
- 通过因数d作为中介,所有具有d因数的数都会被自动连通
2.2 因数筛法预处理
我们可以预先处理每个数的因数:
python复制from math import isqrt
def get_factors(x):
factors = set()
for i in range(1, isqrt(x)+1):
if x % i == 0:
if i > k: factors.add(i)
if (x//i) > k: factors.add(x//i)
return factors
这种预处理的时间复杂度为O(n√n),当n=1e4时完全可接受。
3. 并查集的高效实现
3.1 标准并查集模板
python复制class DSU:
def __init__(self, size):
self.parent = list(range(size+1)) # 1-based索引
self.rank = [0]*(size+1)
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
xr, yr = self.find(x), self.find(y)
if xr == yr: return
if self.rank[xr] < self.rank[yr]:
self.parent[xr] = yr
else:
self.parent[yr] = xr
if self.rank[xr] == self.rank[yr]:
self.rank[xr] += 1
3.2 基于因数的连接策略
我们不需要显式存储所有边,而是:
- 从k+1开始枚举所有可能的因数d
- 对于每个d,将所有d的倍数通过并查集连接起来
python复制dsu = DSU(n)
for d in range(k+1, n+1):
for multiple in range(2*d, n+1, d):
dsu.union(d, multiple)
这个算法的时间复杂度是O(n log n),因为调和级数的性质使得内层循环总次数为n/1 + n/2 + ... + n/n ≈ n ln n。
4. 查询处理与优化
4.1 直接查询实现
对于查询(a,b),只需检查find(a) == find(b):
python复制def areConnected(a, b):
return dsu.find(a) == dsu.find(b)
4.2 查询预处理优化
如果查询次数很多(比如1e5次),可以考虑预处理所有节点的根节点:
python复制root = [dsu.find(i) for i in range(n+1)]
这样查询时可以直接比较root[a]和root[b],将查询时间从O(α(n))降到O(1)。
5. 完整代码实现
python复制class DSU:
def __init__(self, size):
self.parent = list(range(size+1))
self.rank = [0]*(size+1)
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
xr, yr = self.find(x), self.find(y)
if xr == yr: return
if self.rank[xr] < self.rank[yr]:
self.parent[xr] = yr
else:
self.parent[yr] = xr
if self.rank[xr] == self.rank[yr]:
self.rank[xr] += 1
class Solution:
def areConnected(self, n: int, threshold: int, queries: List[List[int]]) -> List[bool]:
if threshold == 0: # 特殊情况处理
return [True]*len(queries)
dsu = DSU(n)
for d in range(threshold+1, n+1):
for multiple in range(2*d, n+1, d):
dsu.union(d, multiple)
return [dsu.find(a) == dsu.find(b) for a,b in queries]
6. 复杂度分析与边界情况
6.1 时间复杂度
- 并查集初始化:O(n)
- 因数连接处理:O(n log n)
- 查询处理:O(q α(n)),其中α是反阿克曼函数
- 总体:O(n log n + q)
6.2 空间复杂度
- 并查集存储:O(n)
- 查询结果:O(q)
- 总体:O(n + q)
6.3 特殊边界情况
- threshold = 0时,所有数都至少被1连通,因此所有查询都应返回True
- n = 1时,任何查询都应返回True(单个节点总是连通自己)
- threshold ≥ n时,没有数对满足gcd > threshold,只有相同节点才连通
7. 实际测试中的注意事项
7.1 性能优化点
- 并查集使用路径压缩和按秩合并,确保接近O(1)的查询复杂度
- 对于threshold=0的情况单独处理,避免不必要的计算
- 在Python中,列表推导式比显式循环更快,特别是在处理查询结果时
7.2 常见错误排查
- 忘记处理1-based和0-based索引的问题(节点编号从1开始)
- 在因数枚举时,错误地从1开始而不是threshold+1
- 并查集的find函数没有正确实现路径压缩,导致超时
- 没有考虑threshold=0的特殊情况,导致错误结果
8. 同类问题扩展
这种基于数学性质优化图连通性的思路可以应用于多种变体问题:
- 修改连通条件:如改为lcm(a,b) > k或a*b > k等
- 动态连通性问题:支持动态添加/删除某些特定连接
- 带权连通性:在连接时考虑边的权重信息
例如,如果题目改为"当a+b > k时连接",我们可以直接按数值大小分类处理,而不需要复杂的因数分解。
