学习笔记:解决差别值分组问题

89 阅读8分钟

问题描述

我们需要将 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. 具体实现步骤
  1. 构建图:对每个差别上限 L,判断哪些人可以连在一起。

  2. 连通分量计算:通过 BFS 或 DFS 统计图中连通分量的数量。

  3. 二分查找 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

关键步骤

  1. 差别上限计算
    • 最大差别上限取决于所有点对之间的最大差别值: max(∣a[i]−a[j]∣+∣b[i]−b[j]∣)
  2. 连通分量统计
    • 利用 BFS 或 DFS 遍历图,统计连通分量的数量。
  3. 二分查找逻辑
    • 初始范围为 [0,max_difference]。
    • 每次尝试中间值 L,验证是否满足 k 个组:
      • 如果满足,说明可以扩大差别上限,尝试更大的 L;
      • 如果不满足,说明 L 太大,需要减小。

复杂度分析

  1. 时间复杂度
    • 图构建:O(n^2),需要计算所有点对的差别值。
    • 连通分量统计:O(n + E),最多为 O(n^2)。
    • 二分查找次数:O(log⁡(max_difference))
    • 总时间复杂度为:O(n^2 * log⁡(max_difference))
  2. 空间复杂度
    • 图的存储需要 O(n^2)。
    • 额外辅助数组(如 visited 和队列)需要 O(n)。
    • 总空间复杂度为 O(n^2)。

测试用例分析

  1. 用例 1

    solution(3, 2, [1, 9, 3], [2, 7, 8])
    
    • 能力值:1,9,3,性格值:2,7,8。
    • 差别上限 L=7 时,分组为:[1] 和 [9,3]。
    • 结果:7
  2. 用例 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. 用例 3

    solution(5, 2, [100, 50, 25, 75, 10], [90, 45, 20, 80, 15])
    
    • 差别上限 L=59 时,可以分成 2 组。
    • 结果:59

总结

  • 本题通过 二分查找 + 图的连通性 高效解决问题。
  • 学习到的关键技巧:
    1. 图的建模:将问题转化为节点和连边的关系。
    2. 连通分量的统计:通过 BFS/DFS 计算图中有多少个独立部分。
    3. 二分查找优化:在范围内寻找最优解。

二分查找的常见问题及解决方法

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 边界问题
  • 问题:二分查找中 lowhigh 的范围不确定可能导致死循环或错误判断。

  • 解决方法

    1. 明确循环条件:一般为 low <= high
    2. 更新指针时要避免遗漏 mid
      • low = mid + 1high = mid - 1
    3. 注意返回值和循环结束后的边界。
3.2 数值溢出问题
  • 问题:计算 mid = (low + high) // 2 时,当 lowhigh 很大时可能导致溢出(在某些语言中,如 C++)。
  • 解决方法: 使用 mid = low + (high - low) // 2
3.3 无限循环问题
  • 问题:更新 lowhigh 时未正确排除当前范围,导致死循环。
  • 解决方法:
    1. 检查循环条件是否合理。
    2. 确保每次更新 lowhigh 时,能有效缩小范围。
3.4 无法找到解
  • 问题:当目标值不存在时,返回值可能不正确。
  • 解决方法:
    1. 明确返回值的语义(找到目标值返回其索引,否则返回 -1 或其他提示)。
    2. lowhigh 的最终状态进行额外检查。

4. 常见的二分查找问题类型

4.1 在排序数组中查找目标值

这是最经典的二分查找问题。

问题描述:给定一个升序数组和目标值 target,判断该值是否存在,并返回索引。

关键点

  • 二分查找直接对中点值与目标值进行比较。
4.2 查找目标值的上下边界

问题描述:给定一个升序数组和目标值 target,找到目标值的最小索引和最大索引。

解决方法

  • 两次二分查找:
    1. 第一次找最左边界,条件是 arr[mid] >= target
    2. 第二次找最右边界,条件是 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。

解决方法

  1. 判断中点在左半部分还是右半部分。
  2. 根据目标值与中点的关系决定缩小范围。
5.2 分配问题

问题描述:将资源分配到不同部分,最小化最大开销。

  • 示例:将书分配给学生,确保阅读量最小化。

解决方法

  • 二分查找最大开销的下限和上限。
  • 定义一个验证函数,判断当前分配方案是否可行。

6. 总结

  • 关键特性:单调性是二分查找的前提。
  • 通用技巧
    1. 写清楚循环条件和指针更新规则。
    2. 通过逻辑推导明确答案在循环结束时的位置。
    3. 对边界情况进行全面测试。
  • 应用场景
    • 搜索问题(目标值、边界值)。
    • 最大化/最小化问题(资源分配、峰值问题)。
    • 模拟条件(判断方案是否可行)。

熟练掌握二分查找的模板和技巧,可以帮助高效解决多种实际问题。