二分查找解决LeetCode 1283最小除数问题

📅 2026/7/27 5:36:22 👁️ 阅读次数 📝 编程学习
二分查找解决LeetCode 1283最小除数问题

1. 问题背景与理解

今天想和大家分享一道LeetCode上的经典二分查找题目——1283. Find the Smallest Divisor Given a Threshold(使结果不超过阈值的最小除数)。这道题在2023年的周赛中频繁出现变种,也是面试中考察二分查找应用的常见题型。

题目要求我们找到一个最小的除数,使得将数组中所有元素除以这个除数(向上取整)后的总和不超过给定的阈值。举个例子,数组nums = [1,2,5,9],阈值threshold = 6。我们需要找到一个最小的除数d,使得ceil(1/d) + ceil(2/d) + ceil(5/d) + ceil(9/d) ≤ 6。

2. 解题思路分析

2.1 暴力解法与复杂度问题

最直观的想法是从1开始逐个尝试除数,计算对应的和是否满足条件。这种方法在最坏情况下需要尝试max(nums)次(因为除数超过数组最大值时,所有ceil(num/d)都为1,总和为数组长度,必然满足条件),时间复杂度为O(n * max(nums)),对于大数组来说效率太低。

2.2 二分查找的适用性

观察到随着除数的增大,总和是单调不增的。这意味着我们可以使用二分查找来高效地找到满足条件的最小除数。具体来说:

  • 如果当前除数d满足条件(sum ≤ threshold),那么更大的除数也一定满足,我们需要尝试更小的除数
  • 如果不满足条件(sum > threshold),则需要尝试更大的除数

这种单调性使得二分查找成为解决此问题的理想选择。

3. 详细实现步骤

3.1 确定搜索范围

二分查找的第一步是确定搜索范围的下界和上界:

  • 下界left:最小可能除数是1(因为除数必须为正整数)
  • 上界right:可以取数组中的最大值,因为当d ≥ max(nums)时,每个ceil(num/d) = 1,总和为数组长度

实际上,我们可以将上界初始化为max(nums),因为更大的除数不会改变结果。

3.2 二分查找框架

标准的二分查找框架如下:

left, right = 1, max(nums) while left < right: mid = (left + right) // 2 if sum(ceil(num / mid) for num in nums) <= threshold: right = mid else: left = mid + 1 return left

3.3 计算sum的优化

计算sum时,ceil(num / d)可以改写为(num + d - 1) // d,这样避免了浮点数运算,效率更高。完整的优化实现:

def smallestDivisor(nums, threshold): left, right = 1, max(nums) while left < right: mid = (left + right) // 2 total = sum((num + mid - 1) // mid for num in nums) if total <= threshold: right = mid else: left = mid + 1 return left

4. 复杂度分析

  • 时间复杂度:O(n log max(nums)),其中n是数组长度。每次计算sum需要O(n)时间,二分查找需要进行O(log max(nums))次迭代。
  • 空间复杂度:O(1),只使用了常数个额外空间。

5. 边界条件与测试用例

5.1 常见测试用例

# 示例1 nums = [1,2,5,9] threshold = 6 # 输出: 5 # 示例2 nums = [44,22,33,11,1] threshold = 5 # 输出: 44 # 示例3 nums = [21212,10101,12121] threshold = 1000000 # 输出: 1

5.2 特殊边界情况

  • 数组长度为1时:直接返回ceil(num / threshold)
  • 阈值等于数组长度时:返回max(nums)
  • 所有元素相同的情况
  • 包含大数的情况(测试整数溢出)

6. 常见错误与调试技巧

6.1 常见错误

  1. 初始上界设置过小:如果right初始值小于实际需要的最大值,可能找不到解
  2. 二分查找终止条件错误:可能导致死循环或错过正确解
  3. 整数溢出:在大数情况下,(left + right)可能溢出,应使用left + (right - left) // 2

6.2 调试技巧

  • 打印每次迭代的left, right和mid值,观察搜索范围变化
  • 对于错误用例,手动计算中间结果验证
  • 使用小测试用例逐步调试

7. 相关题目拓展

这道题与以下LeetCode题目思路类似,都可以用二分查找解决:

    1. Koko Eating Bananas(爱吃香蕉的狒狒)
    1. Capacity To Ship Packages Within D Days
    1. Split Array Largest Sum

这些问题的共同特点是:

  1. 需要找到一个最小/最大的满足条件的值
  2. 存在单调性关系(随着候选值的增大/减小,条件满足情况单调变化)
  3. 直接计算单个候选值的代价相对较小

8. 实际应用场景

这类问题在实际中有广泛应用,例如:

  1. 资源分配:确定最小资源单位以满足多个任务需求
  2. 负载均衡:找到最小处理能力使服务器负载不超过阈值
  3. 数据分片:确定最小分片大小使查询时间不超过限制

理解这类问题的解法有助于解决实际工程中的优化问题。

9. 不同语言实现要点

9.1 C++实现

int smallestDivisor(vector<int>& nums, int threshold) { int left = 1, right = *max_element(nums.begin(), nums.end()); while (left < right) { int mid = left + (right - left) / 2; int total = 0; for (int num : nums) { total += (num + mid - 1) / mid; } if (total <= threshold) { right = mid; } else { left = mid + 1; } } return left; }

9.2 Java实现

public int smallestDivisor(int[] nums, int threshold) { int left = 1, right = Arrays.stream(nums).max().getAsInt(); while (left < right) { int mid = left + (right - left) / 2; int sum = 0; for (int num : nums) { sum += (num + mid - 1) / mid; } if (sum <= threshold) { right = mid; } else { left = mid + 1; } } return left; }

10. 性能优化技巧

  1. 提前终止:如果在计算sum过程中发现已经超过阈值,可以提前终止计算
  2. 并行计算:对于大数组,可以并行计算各个元素的ceil值
  3. 预处理:如果需要对同一个数组多次查询不同阈值,可以预处理排序

11. 二分查找变种讨论

这道题使用了二分查找的"寻找第一个满足条件的值"的变种。类似的二分查找变种包括:

  1. 寻找第一个大于等于target的值
  2. 寻找最后一个小于等于target的值
  3. 在旋转排序数组中查找
  4. 在无限序列中查找

理解这些变种对于解决复杂的二分查找问题很有帮助。

12. 数学性质深入分析

这个问题本质上是在寻找满足条件的最小整数d,使得:

Σ⌈nums[i]/d⌉ ≤ threshold

我们可以从数学上分析这个不等式的性质:

  1. 当d增加时,每个⌈nums[i]/d⌉单调不增
  2. 总和Σ⌈nums[i]/d⌉也是单调不增的
  3. 最小d满足Σ⌈nums[i]/d⌉ ≤ threshold,而d-1不满足

这种单调性保证了二分查找的正确性。

13. 实际工程中的应用实例

假设我们有一个视频处理系统,需要将多个视频分片转码。每个视频分片有不同的大小nums[i],我们的转码集群有固定的处理能力threshold。我们需要找到一个最小的分片大小d,使得:

⌈nums[i]/d⌉表示将第i个视频分片分成多少块 总和Σ⌈nums[i]/d⌉表示总共需要处理的任务数 我们需要确保总任务数不超过集群的处理能力threshold

这正是我们解决的问题在实际工程中的一个应用场景。

14. 测试用例生成策略

为了全面测试代码的正确性,可以生成以下类型的测试用例:

  1. 随机小数组:测试基本逻辑
  2. 大数组小阈值:测试性能
  3. 大数组大阈值:测试边界条件
  4. 所有元素相同:测试特殊情况
  5. 递增/递减序列:测试单调情况
  6. 包含1和极大值的数组:测试极端情况

15. 复杂度对比与算法选择

将二分查找解法与暴力解法进行对比:

方法时间复杂度空间复杂度适用场景
暴力解法O(n * max(nums))O(1)小数组
二分查找O(n log max(nums))O(1)大数组

对于max(nums)很大的情况,二分查找的优势非常明显。例如,当max(nums)=10^6,n=10^5时:

  • 暴力解法:10^11次操作
  • 二分查找:约2*10^6次操作

16. 可视化理解

我们可以将问题可视化:

  1. 绘制d从1到max(nums)时,总和Σ⌈nums[i]/d⌉的变化曲线
  2. 曲线是阶梯状递减的
  3. 我们需要找到曲线与threshold水平线相交的最左边的d值

这种可视化有助于理解二分查找为什么适用于此问题。

17. 二分查找模板总结

这类问题的通用二分查找模板:

def binary_search_template(nums, threshold): left, right = min_bound, max_bound # 根据问题确定边界 while left < right: mid = (left + right) // 2 if condition(mid): # 满足条件 right = mid else: left = mid + 1 return left

对于本题:

  • min_bound = 1
  • max_bound = max(nums)
  • condition(mid) = sum(ceil(num / mid) for num in nums) ≤ threshold

18. 代码风格与最佳实践

  1. 使用有意义的变量名:left/right比l/r更清晰
  2. 添加注释解释关键步骤
  3. 处理边界情况的防御性编程
  4. 使用Python的生成器表达式提高代码可读性
  5. 添加类型提示(Python 3.6+)

改进后的代码:

from typing import List def smallestDivisor(nums: List[int], threshold: int) -> int: """返回使结果不超过阈值的最小除数""" left, right = 1, max(nums) while left < right: mid = (left + right) // 2 # 计算向上取整的总和 total = sum((num + mid - 1) // mid for num in nums) if total <= threshold: right = mid # 尝试更小的除数 else: left = mid + 1 # 需要更大的除数 return left

19. 语言特性利用

在不同语言中,可以利用语言特性简化代码:

19.1 Python中的优化

# 使用math.ceil import math total = sum(math.ceil(num / mid) for num in nums) # 或者使用负数的地板除 total = sum(-(-num // mid) for num in nums)

19.2 Java中的Stream API

int sum = Arrays.stream(nums) .map(num -> (num + mid - 1) / mid) .sum();

20. 多维度扩展思考

这个问题可以从多个维度进行扩展:

  1. 如果nums[i]和threshold都很大(比如1e9),如何避免整数溢出?
  2. 如果要求结果是浮点数(d可以有小数部分),如何修改算法?
  3. 如果除了除数的限制,还有其他的约束条件,如何调整算法?
  4. 如果数组是动态变化的,如何高效维护结果?

这些扩展问题可以帮助深入理解算法并应对更复杂的场景。

21. 历史与变种

这道题是二分查找经典问题的变种,类似的思路在计算机科学历史上早有应用:

  1. 资源分配问题(1970年代)
  2. 调度问题(1980年代)
  3. 最近在机器学习中的超参数搜索也有应用

理解问题的历史背景有助于把握其本质。

22. 面试技巧

在面试中遇到此类问题时:

  1. 先明确问题要求,举例说明
  2. 分析暴力解法的不足
  3. 提出二分查找的思路并证明其正确性
  4. 讨论边界条件和特殊情况
  5. 逐步编写代码并解释
  6. 分析时间空间复杂度
  7. 提出可能的优化和扩展

23. 学习资源推荐

  1. 《算法导论》中的二分查找章节
  2. LeetCode二分查找专题
  3. Topcoder二分查找教程
  4. 算法可视化网站(如VisuAlgo)

24. 个人心得

在实际解决这个问题时,我有几点体会:

  1. 确定单调性是应用二分查找的关键
  2. 初始搜索范围的设置对效率有重要影响
  3. 向上取整的计算方式有多种,选择最高效的
  4. 测试用例要覆盖各种特殊情况
  5. 在面试中,沟通思路比直接写代码更重要