问题描述
我们需要将 n 个人分成至少 k 个非空小组,每个人有两个属性:
- 能力值 a[i]
- 性格值 b[i]
两个人之间的差别值定义为:
∣a[i]−a[j]∣+∣b[i]−b[j]∣
约束:
- 如果两个人的差别值不超过某个差别上限 L,他们必须属于同一组。
- 问题目标是求出满足至少 k个组的前提下,差别上限 L 的最大值。
解题思路
1. 问题分析
- 差别上限 L 越大,分组的限制越少,最终的组数会更少。
- 差别上限 L 越小,分组限制越多,需要分更多组。
- 我们可以利用 二分查找 来寻找符合条件的最大 L。
2. 核心思想
-
使用
图的连通性
来建模。
- 每个人看作一个节点。
- 如果两个人的差别值 ≤L,在他们之间建立一条边。
- 连通分量的数量即为组数。
-
通过 二分查找 尝试不同的 L,在满足至少 k 个组的条件下找到最大的 L。
3. 具体实现步骤
-
构建图:对每个差别上限 L,判断哪些人可以连在一起。
-
连通分量计算:通过 BFS 或 DFS 统计图中连通分量的数量。
-
二分查找 L
:
- 如果某个 L 可以将所有人分成至少 k 个组,尝试更大的 L。
- 否则,尝试更小的 L。
代码实现
from collections import defaultdict, deque
def solution(n: int, k: int, a: list, b: list) -> int:
def is_valid(L):
# 构建差别上限为 L 的图
adj = defaultdict(list)
for i in range(n):
for j in range(i + 1, n):
if abs(a[i] - a[j]) + abs(b[i] - b[j]) <= L:
adj[i].append(j)
adj[j].append(i)
# 计算连通分量数量
visited = [False] * n
components = 0
def bfs(node):
queue = deque([node])
while queue:
cur = queue.popleft()
for neighbor in adj[cur]:
if not visited[neighbor]:
visited[neighbor] = True
queue.append(neighbor)
for i in range(n):
if not visited[i]:
visited[i] = True
components += 1
bfs(i)
return components >= k
# 二分查找差别上限
low, high = 0, max(abs(a[i] - a[j]) + abs(b[i] - b[j]) for i in range(n) for j in range(n))
answer = 0
while low <= high:
mid = (low + high) // 2
if is_valid(mid):
answer = mid # 更新答案
low = mid + 1
else:
high = mid - 1
return answer
关键步骤
- 差别上限计算:
- 最大差别上限取决于所有点对之间的最大差别值: max(∣a[i]−a[j]∣+∣b[i]−b[j]∣)
- 连通分量统计:
- 利用 BFS 或 DFS 遍历图,统计连通分量的数量。
- 二分查找逻辑:
- 初始范围为 [0,max_difference]。
- 每次尝试中间值 L,验证是否满足 k 个组:
- 如果满足,说明可以扩大差别上限,尝试更大的 L;
- 如果不满足,说明 L 太大,需要减小。
复杂度分析
- 时间复杂度:
- 图构建:O(n^2),需要计算所有点对的差别值。
- 连通分量统计:O(n + E),最多为 O(n^2)。
- 二分查找次数:O(log(max_difference))
- 总时间复杂度为:O(n^2 * log(max_difference))
- 空间复杂度:
- 图的存储需要 O(n^2)。
- 额外辅助数组(如
visited和队列)需要 O(n)。 - 总空间复杂度为 O(n^2)。
测试用例分析
-
用例 1:
solution(3, 2, [1, 9, 3], [2, 7, 8])- 能力值:1,9,3,性格值:2,7,8。
- 差别上限 L=7 时,分组为:[1] 和 [9,3]。
- 结果:7。
-
用例 2:
solution(4, 3, [10, 20, 30, 40], [5, 15, 25, 35])- 能力值:10,20,30,40性格值:5,15,25,35。
- 差别上限 L=19 时,分组为:[10,20],[30],[40]。
- 结果:19。
-
用例 3:
solution(5, 2, [100, 50, 25, 75, 10], [90, 45, 20, 80, 15])- 差别上限 L=59 时,可以分成 2 组。
- 结果:59。
总结
- 本题通过 二分查找 + 图的连通性 高效解决问题。
- 学习到的关键技巧:
- 图的建模:将问题转化为节点和连边的关系。
- 连通分量的统计:通过 BFS/DFS 计算图中有多少个独立部分。
- 二分查找优化:在范围内寻找最优解。
二分查找的常见问题及解决方法
1. 如何判断是否可以使用二分查找?
二分查找适用于 具有单调性的问题,即:
- 当答案或者搜索目标满足某种 单调递增 或 单调递减 的特性。
- 通过划分范围,可以排除一半不符合条件的解。
示例场景:
- 在排序数组中查找目标值。
- 找到满足条件的最小/最大值,例如分配任务、容量规划、分组问题等。
2. 二分查找的基本模板
模板 1:查找目标值
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high: # 注意边界是 <=
mid = (low + high) // 2
if arr[mid] == target:
return mid # 找到目标值
elif arr[mid] < target:
low = mid + 1 # 排除左半部分
else:
high = mid - 1 # 排除右半部分
return -1 # 未找到目标值
模板 2:查找满足条件的最小值/最大值
def binary_search_min_max(arr, condition):
low, high = 0, len(arr) - 1
answer = -1 # 存储最终答案
while low <= high:
mid = (low + high) // 2
if condition(mid): # 判断条件是否满足
answer = mid # 更新答案
high = mid - 1 # 尝试更小的值
else:
low = mid + 1 # 尝试更大的值
return answer
3. 常见问题
3.1 边界问题
-
问题:二分查找中
low和high的范围不确定可能导致死循环或错误判断。 -
解决方法
:
- 明确循环条件:一般为
low <= high。 - 更新指针时要避免遗漏 mid:
low = mid + 1或high = mid - 1。
- 注意返回值和循环结束后的边界。
- 明确循环条件:一般为
3.2 数值溢出问题
- 问题:计算
mid = (low + high) // 2时,当low和high很大时可能导致溢出(在某些语言中,如 C++)。 - 解决方法: 使用
mid = low + (high - low) // 2。
3.3 无限循环问题
- 问题:更新
low或high时未正确排除当前范围,导致死循环。 - 解决方法:
- 检查循环条件是否合理。
- 确保每次更新
low或high时,能有效缩小范围。
3.4 无法找到解
- 问题:当目标值不存在时,返回值可能不正确。
- 解决方法:
- 明确返回值的语义(找到目标值返回其索引,否则返回 -1 或其他提示)。
- 对
low和high的最终状态进行额外检查。
4. 常见的二分查找问题类型
4.1 在排序数组中查找目标值
这是最经典的二分查找问题。
问题描述:给定一个升序数组和目标值 target,判断该值是否存在,并返回索引。
关键点:
- 二分查找直接对中点值与目标值进行比较。
4.2 查找目标值的上下边界
问题描述:给定一个升序数组和目标值 target,找到目标值的最小索引和最大索引。
解决方法:
- 两次二分查找:
- 第一次找最左边界,条件是
arr[mid] >= target。 - 第二次找最右边界,条件是
arr[mid] <= target。
- 第一次找最左边界,条件是
4.3 最大化/最小化问题
问题描述:寻找满足某种条件的最大值或最小值。
- 示例 1:分配任务最小化最大工作量。
- 示例 2:生产线问题中找到最低成本的工作分配。
解决方法:
- 定义一个判定函数
condition(x),判断当前值是否满足要求。 - 使用二分查找逐步逼近满足条件的最优解。
4.4 数组中的峰值
问题描述:在一个数组中找到一个峰值(比邻近元素大的值)。
- 数组未必排序,但可以利用单调性。
解决方法:
- 对中点值与相邻值比较:
- 若 arr[mid]>arr[mid+1],峰值可能在左侧。
- 否则,峰值可能在右侧。
5. 二分查找的扩展问题
5.1 搜索旋转排序数组
问题描述:一个升序数组被部分旋转后,需要在其中查找目标值。
- 示例:4,5,6,7,0,1,2中查找目标值 0。
解决方法:
- 判断中点在左半部分还是右半部分。
- 根据目标值与中点的关系决定缩小范围。
5.2 分配问题
问题描述:将资源分配到不同部分,最小化最大开销。
- 示例:将书分配给学生,确保阅读量最小化。
解决方法:
- 二分查找最大开销的下限和上限。
- 定义一个验证函数,判断当前分配方案是否可行。
6. 总结
- 关键特性:单调性是二分查找的前提。
- 通用技巧:
- 写清楚循环条件和指针更新规则。
- 通过逻辑推导明确答案在循环结束时的位置。
- 对边界情况进行全面测试。
- 应用场景:
- 搜索问题(目标值、边界值)。
- 最大化/最小化问题(资源分配、峰值问题)。
- 模拟条件(判断方案是否可行)。
熟练掌握二分查找的模板和技巧,可以帮助高效解决多种实际问题。