LeetCode 3296. 使山区高度为零的最少秒数
题目描述
给定一个整数 mountainHeight 表示山的高度,以及一个数组 workerTimes 表示每个工人降低单位高度所需的基础时间。工人降低高度的规则如下:
- 工人降低第 1 个单位高度需要
workerTime秒; - 降低第 2 个单位需要
2 * workerTime秒; - 降低第 3 个单位需要
3 * workerTime秒; - ......
- 降低第 k 个单位需要
k * workerTime秒。
因此,一个工人降低 k 个单位高度所需的总时间为:
time=workerTime×(1+2+⋯+k)=workerTime×k(k+1)2. \text{time} = \text{workerTime} \times (1 + 2 + \cdots + k) = \text{workerTime} \times \frac{k(k+1)}{2}. time=workerTime×(1+2+⋯+k)=workerTime×2k(k+1).
所有工人可以同时工作 ,目标是使所有工人降低的总高度至少 为 mountainHeight。求所需的最少秒数。
思路分析
这是一个典型的二分答案 问题。我们直接二分最终的时间 T,判断在 T 秒内,所有工人最多能降低的总高度是否 ≥ mountainHeight。如果可行,则尝试更小的时间;否则需要增大时间。
如何判断给定时间 T 内工人能降低的高度?
对于某个工人,其基础时间为 t,我们需要找到最大的整数 k,使得该工人在 T 秒内能完成 k 个单位的工作,即:
t⋅k(k+1)2≤T. t \cdot \frac{k(k+1)}{2} \le T. t⋅2k(k+1)≤T.
变形得:
k(k+1)2≤Tt. \frac{k(k+1)}{2} \le \frac{T}{t}. 2k(k+1)≤tT.
令 work = T / t(注意这里除法是整数除法,但推导公式时用实数),则问题转化为求最大的 k 满足:
k(k+1)≤2⋅work. k(k+1) \le 2 \cdot work. k(k+1)≤2⋅work.
即:
k2+k−2⋅work≤0. k^2 + k - 2 \cdot work \le 0. k2+k−2⋅work≤0.
这是一个一元二次不等式,其正根为:
k=−1+1+8⋅work2. k = \frac{-1 + \sqrt{1 + 8 \cdot work}}{2}. k=2−1+1+8⋅work .
由于 k 必须是整数,我们取该值的整数部分(向下取整)。注意,由于浮点数计算可能存在精度误差,我们可以在开方后加上一个很小的 eps(如 1e-7)再取整,确保不会因为微小的舍入导致结果偏小。
得到每个工人的 k 后,累加所有工人的 k 值,即为总降低高度 cnt。
二分上下界的确定
- 下界 :至少需要 1 秒,取
l = 1。 - 上界 :考虑最坏情况------只用最慢的工人(即
maxWorkerTimes)独自完成全部mountainHeight个单位所需的时间。该工人降低全部高度所需时间为:
maxWorkerTimes×mountainHeight×(mountainHeight+1)2. \text{maxWorkerTimes} \times \frac{\text{mountainHeight} \times (\text{mountainHeight} + 1)}{2}. maxWorkerTimes×2mountainHeight×(mountainHeight+1).
这个值一定大于或等于实际最优解,因此可以作为安全的上界。
二分过程
在区间 [l, r] 上进行二分查找,每次取中点 mid,计算总降低高度 cnt:
- 若
cnt >= mountainHeight,说明mid可行,记录答案并尝试更小的时间(r = mid - 1); - 否则,需要增大时间(
l = mid + 1)。
最终 ans 即为最小秒数。
代码实现(C++)
cpp
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
class Solution {
private:
static constexpr double eps = 1e-7; // 用于防止浮点误差
public:
long long minNumberOfSeconds(int mountainHeight, vector<int>& workerTimes) {
// 找到最慢工人的时间,用于计算二分上界
int maxWorkerTimes = *max_element(workerTimes.begin(), workerTimes.end());
// 二分上下界
long long l = 1;
long long r = static_cast<long long>(maxWorkerTimes) * mountainHeight * (mountainHeight + 1) / 2;
long long ans = 0;
while (l <= r) {
long long mid = l + (r - l) / 2; // 避免溢出
long long totalHeight = 0; // 所有工人在 mid 秒内能降低的总高度
for (int t : workerTimes) {
// work = mid / t 表示该工人能完成的"工作量"(即 k(k+1)/2 的最大值)
long long work = mid / t;
// 解二次方程求最大 k: k = floor( (-1 + sqrt(1 + 8*work)) / 2 )
// 加 eps 避免浮点误差导致取整偏小
long long k = static_cast<long long>((-1.0 + sqrt(1 + 8.0 * work)) / 2 + eps);
totalHeight += k;
// 如果已经超过目标高度,可以提前结束循环(优化)
if (totalHeight >= mountainHeight) break;
}
if (totalHeight >= mountainHeight) {
ans = mid;
r = mid - 1; // 尝试更小的时间
} else {
l = mid + 1; // 时间不够,增大
}
}
return ans;
}
};
复杂度分析
- 时间复杂度 :二分查找次数为 O(log(上界))O(\log(\text{上界}))O(log(上界)),每次检查需要遍历所有工人(设工人数为 nnn),因此总时间复杂度为 O(nlog(maxWorkerTimes⋅mountainHeight2))O(n \log(\text{maxWorkerTimes} \cdot \text{mountainHeight}^2))O(nlog(maxWorkerTimes⋅mountainHeight2))。在题目给定范围内可以接受。
- 空间复杂度 :仅使用常数个变量,O(1)O(1)O(1)。
注意事项
- 整数溢出 :计算上界时,
maxWorkerTimes和mountainHeight都可能较大,乘积可能超出 32 位整数范围,因此使用long long存储。 - 浮点精度 :公式 k=−1+1+8⋅work2k = \frac{-1 + \sqrt{1 + 8 \cdot \text{work}}}{2}k=2−1+1+8⋅work 涉及开方,由于浮点数运算可能产生微小误差,直接取整可能导致 k 比实际值小 1。加上一个很小的
eps(如1e-7)可以修正这种误差,保证正确性。 - 提前退出 :在遍历工人时,如果
totalHeight已经达到或超过mountainHeight,可以提前结束循环,节省时间。 - 二分写法 :使用
l <= r的闭区间写法,注意更新边界时mid已经检查过,因此l = mid + 1或r = mid - 1。
总结
本题的关键在于将问题转化为二分答案,并利用二次方程求根快速计算每个工人在给定时间内的最大工作量。这种方法避免了直接模拟的复杂性,同时利用了数学推导,使算法高效且易于实现。
理解该题后,可以进一步思考如果工人降低单位时间不是等差数列而是其他规律(如指数增长),应如何调整判定函数。二分答案的思路依然通用,只需改变内部的计算公式即可。