1. 问题背景与核心挑战
这道LeetCode题目描述了一个有趣的"移山"场景:给定一座山的高度mountainHeight和一组工人的工作效率workerTimes,我们需要计算工人们同时工作时将山完全移平所需的最少秒数。每个工人i每降低x单位高度所需的时间遵循特定公式:workerTimes[i] + workerTimes[i]*2 + ... + workerTimes[i]*x。
这个问题的核心在于理解两个关键点:
- 工作时间计算的非线性特性:工人i降低x单位高度的时间不是简单的线性关系,而是1到x的累加和乘以workerTimes[i]
- 并行工作的最优化:所有工人同时工作,最终耗时取决于最慢的那个工人
2. 问题分析与数学建模
2.1 工作时间公式解析
对于工人i降低x单位高度,所需时间可以表示为:
T_i = workerTimes[i] * (1 + 2 + 3 + ... + x) = workerTimes[i] * x(x+1)/2
这是一个二次函数关系,意味着随着x的增加,所需时间会快速增长。例如:
- 降低1单位:T = workerTimes[i] * 1
- 降低2单位:T = workerTimes[i] * 3
- 降低3单位:T = workerTimes[i] * 6
2.2 并行工作的时间计算
由于所有工人同时工作,整个移山过程的总时间取决于最慢的那个工人完成其分配任务的时间。因此,我们需要找到一个分配方案,使得:
- 所有工人分配的高度降低量之和 ≥ mountainHeight
- 所有工人中最大的工作时间尽可能小
这实际上是一个典型的"最小化最大负载"问题,类似于分布式计算中的任务分配问题。
3. 解题思路与算法选择
3.1 暴力枚举的不可行性
最直观的想法可能是枚举所有可能的高度分配方案,然后计算每种方案的最大工作时间,最后取最小值。然而,这种方法的时间复杂度极高,对于mountainHeight=10^5和workerTimes.length=10^4的情况,计算量会爆炸。
3.2 二分搜索的适用性
观察到这个问题具有单调性:如果给定时间T,我们可以计算每个工人在T时间内最多能降低多少高度。如果所有工人能降低的总高度≥mountainHeight,则T可能是一个可行解;否则需要更多时间。
这种"给定解验证可行性"的特性非常适合二分搜索算法。我们可以:
- 确定可能的时间范围[L, R]
- 对于中间值mid,验证是否可以在mid时间内完成移山
- 根据验证结果调整搜索范围
3.3 数学推导与关键公式
对于给定的时间T和工人i的工作效率wTime,我们需要求解最大的x使得:
wTime * x(x+1)/2 ≤ T
这是一个二次不等式,可以转化为求方程x² + x - 2T/wTime = 0的正根。使用求根公式:
x = [-1 + sqrt(1 + 8T/wTime)] / 2
由于x必须是整数,我们取这个正根的整数部分作为工人i在时间T内能降低的最大高度。
4. 算法实现细节
4.1 二分搜索边界确定
初始搜索范围的选择很重要:
- 下界L=0(最少需要0时间)
- 上界R需要足够大以覆盖最坏情况。根据题目约束,最坏情况是:
- mountainHeight=10^5
- 只有一个工人,workerTimes[i]=10^6
- 需要解10^6 * x(x+1)/2 ≥ 10^5
- 计算得x≈14,所以R=10^61415/2=1.05×10^8
- 为安全起见,代码中使用R=1e16
4.2 可行性验证函数
对于给定的时间mid_time,计算所有工人能降低的总高度Sum_X:
- 对每个工人,计算count = mid_time / workerTimes[i]
- 使用求根公式计算x = (sqrt(1 + 8*count) - 1) / 2
- 累加所有工人的x值得到Sum_X
如果Sum_X ≥ mountainHeight,则mid_time是一个可行解,可以尝试更小的时间;否则需要更大的时间。
4.3 二分搜索实现
标准的二分搜索模板:
- 初始化L=0,R=1e16
- while(L < R):
- mid = (L + R) / 2
- 计算Sum_X
- if Sum_X ≥ mountainHeight: R = mid
- else: L = mid + 1
- 返回L作为最小时间
5. 代码实现与优化
5.1 完整代码解析
cpp复制class Solution {
public:
long long minNumberOfSeconds(int mountainHeight, vector<int>& workerTimes) {
long long L = 0, R = 1e16;
while(L < R) {
long long mid_time = (L + R) / 2;
long long Sum_X = 0;
for(auto wTime : workerTimes) {
long long count = mid_time / wTime;
Sum_X += (sqrt(1 + 8 * count) - 1) / 2;
}
if(Sum_X >= mountainHeight) R = mid_time;
else L = mid_time + 1;
}
return L;
}
};
5.2 关键点说明
- 使用long long类型防止整数溢出
- 二分搜索终止条件是L < R,确保找到最小解
- 对于每个工人,计算count = mid_time / wTime时,整数除法会自动向下取整
- 求根公式中的sqrt操作可能产生浮点数,但最终结果会被强制转换为整数
5.3 性能分析
- 时间复杂度:O(n log R),其中n是workerTimes的长度,R是搜索范围
- 空间复杂度:O(1),仅使用常数额外空间
对于题目给定的约束条件(n≤10^4, R≤1e16),这个算法效率很高。
6. 边界情况与测试用例
6.1 示例测试
示例1:
mountainHeight = 4, workerTimes = [2,1,1]
- 工人0:2秒降1
- 工人1:3秒降2
- 工人2:1秒降1
- 最大时间3秒
示例2:
mountainHeight = 10, workerTimes = [3,2,2,4]
- 工人0:9秒降2
- 工人1:12秒降3
- 工人2:12秒降3
- 工人3:12秒降2
- 最大时间12秒
示例3:
mountainHeight = 5, workerTimes = [1]
- 唯一工人需要15秒降5
- 最大时间15秒
6.2 极端情况测试
-
山高极大,工人极少:
mountainHeight=1e5, workerTimes=[1e6]
预期结果:约1e16量级 -
工人效率极高:
mountainHeight=100, workerTimes=[1,1,1,...,1] (100个1)
预期结果:约14秒(每个工人降4单位) -
工人效率差异大:
mountainHeight=100, workerTimes=[1,100]
预期结果:需要平衡分配,让高效工人做更多工作
7. 算法优化与变种
7.1 搜索范围优化
初始的R=1e16虽然安全但可能过大。可以更精确地计算上界:
- 找到效率最高的工人(最小的workerTimes[i])
- 计算仅由该工人完成全部工作所需时间
- 作为R的初始值
7.2 浮点数精度处理
在计算(sqrt(1 + 8*count) - 1)/2时,浮点数运算可能引入精度误差。可以考虑:
- 使用更高精度的浮点类型
- 或者使用整数运算近似计算平方根
7.3 并行计算优化
对于非常大的workerTimes数组,可以:
- 并行计算每个工人的贡献
- 使用前缀和等技巧加速累加过程
8. 实际应用与扩展
这个问题虽然以"移山"为背景,但实际上可以应用于许多资源分配和任务调度场景,如:
- 云计算中的任务分配
- 工厂生产线平衡
- 分布式计算任务划分
关键思想都是"在并行环境下,最小化最慢节点的完成时间"。
如果将问题扩展:
- 每个工人有不同的启动时间
- 山的高度降低不是连续的
- 工人之间有依赖关系
这些变种会引入更复杂的算法需求,如动态规划或贪心算法。