【大厂机试题解法笔记】食堂供餐
题目
某公司员工食堂以盒饭方式供餐。
为将员工取餐排队时间降低为 0,食堂的供餐速度必须要足够快。
现在需要根据以往员工取餐的统计信息,计算出一个刚好能达成排队时间为 0 的最低供餐速度。即,食堂在每个单位时间内必须至少做出多少份盒饭才能满足要求。
输入描述
第 1 行为一个正整数 N,表示食堂开餐时长。
1 ≤ N ≤ 1000
第 2 行为一个正整数 M,表示开餐前食堂已经准备好的盒饭份数。
0 ≤ M ≤ 1000
第 3 行为 N 个正整数,用空格分隔,依次表示开餐时间内按时间顺序每个单位时间进入食堂取餐的人数 Pi。
1 ≤ i ≤ N
0 ≤ Pi ≤ 100
备注
每人只取一份盒饭。
需要满足排队时间为 0,必须保证取餐员工到达食堂时,食堂库存盒饭数量不少于本次来取餐的人数。
第一个单位时间来取餐的员工只能取开餐前食堂准备好的盒饭。
每个单位时间里制作的盒饭只能供应给后续单位时间来取餐的员工。
食堂在每个单位时间里制作的盒饭数量是相同的。
输出描述
一个整数,能满足题目要求的最低供餐速度(每个单位时间需要做出多少份盒饭 )。
用例
输入 | 输出 | 说明 |
---|---|---|
3 14 10 4 5 | 3 | 本样例中,总共有 3 批员工就餐,每批人数分别为 10、4、5。 开餐前食堂库存 14 份。 食堂每个单位时间至少要做出 3 份饭才能达成排队时间为 0 的目标。具体情况如下: 第一个单位时间来的 10 位员工直接从库存取餐。取餐后库存剩余 4 份盒饭,加上第一个单位时间做出的 3 份,库存有 7 份。 第二个单位时间来的 4 员工从库存的 7 份中取 4 份。取餐后库存剩余 3 份盒饭,加上第二个单位时间做出的 3 份,库存有 6 份。 第三个单位时间来的员工从库存的 6 份中取 5 份,库存足够。 如果食堂在单位时间只能做出 2 份饭,则情况如下: 第一个单位时间来的 10 位员工直接从库存取餐。取餐后库存剩余 4 份盒饭,加上第一个单位时间做出的 2 份,库存有 6 份。 第二个单位时间来的 4 员工从库存的 6 份中取 4 份。取餐后库存剩余 2 份盒饭,加上第二个单位时间做出的 2 份,库存有 4 份。 |
思考
最低供餐速度,这个速度要满足食堂在当前单位时间做的盒饭数量满足下一轮员工取餐量。第一行输入的开餐时长应该表示单位时间数量,也就是员工取餐轮数。如果食堂库存盒饭满足所有员工取餐量则最低供餐速度是 0,直接返回结果 0, 最大单位供餐数量 Max 肯定是所有员工数量总和减去库存数量。那么题目要求的最低供餐速度可以介于区间 [1, Max] 之间。暴力解法怎么做?是不是枚举这个区间中的每个数,看它是否满足所有员工的取餐量。可以从 1 开始往更多的取餐量枚举,但这不是聪明的枚举,所以应该想到用二分查找枚举效率更高。设置二分查找的左边界为 1, 右边界为 Max,每次计算中间值 mid ,定义函数 canSatisfy(n) 计算是否能满足所有员工取餐量,如果不满足则表明供餐速度不够,把左边边界设为 mid + 1;如果满足供餐,则把右边界设为 mid。至于二分查找这些约束条件很容易写错,如 while (l < r) 而不是 while (l <= r),r = mid 而不是 r = mid -1,记录下:
1. 为什么循环条件是 l < r
而不是 l <= r
?
-
二分查找的目标:找到满足条件的最小供餐速度。当
l
和r
相遇时(即l == r
),这个值就是我们要找的答案,因此循环可以在此终止。 -
避免无限循环:如果使用
l <= r
,在某些情况下可能导致l
和r
在最后一步相互调整时陷入无限循环。例如,当l
和r
相邻时,mid
可能等于l
,若此时更新r = mid
,则r
不变,导致循环无法终止。
2. 为什么 canSatisfy(mid)
为 true
时,右边界 r = mid
而不是 r = mid - 1
?
-
保留候选值:当
mid
满足条件时,它可能就是最小的可行解,因此不能直接排除。将r
更新为mid
可以保留这个候选值,继续在左半部分寻找更小的解。 -
确保覆盖所有可能解:如果使用
r = mid - 1
,可能会跳过真正的最小解。例如,当mid
恰好是最小可行解时,这样的更新会将其排除,导致最终结果偏大。
算法过程
-
输入读取与预处理
- 读取开餐时长 N、初始盒饭数 M,以及每个单位时间的取餐人数数组 P。
- 计算总需求 \(\text{total} = \sum_{i=1}^N P_i\),并确定供餐速度的上界 \(\text{max} = \max(\text{total} - M, 0)\)。若 \(\text{max} = 0\),直接返回 0(初始库存足够)。
-
二分查找最小可行速度
- 初始化搜索区间:下界 \(l = 1\),上界 \(r = \text{max}\)。
- 循环条件:当 \(l < r\) 时,执行以下步骤:
- 计算中间值 \(\text{mid} = \left\lfloor \frac{l + r}{2} \right\rfloor\)。
- 检查可行性:调用函数
canSatisfy(mid)
模拟每个单位时间的库存变化,验证是否满足取餐需求:- 初始库存为 M。
- 遍历每个单位时间 i:
- 若当前库存不足 \(P_i\),返回
false
。 - 否则,扣除 \(P_i\) 份盒饭,并补充 \(\text{mid}\) 份(供后续使用)。
- 若当前库存不足 \(P_i\),返回
- 调整区间:
- 若
mid
可行,说明可能存在更小解,更新 \(r = \text{mid}\)。 - 若
mid
不可行,需增大速度,更新 \(l = \text{mid} + 1\)。
- 若
- 终止条件:当 \(l = r\) 时,返回 r 作为最小可行速度。
-
输出结果
- 二分查找结束后,直接输出 r,即为满足条件的最小供餐速度。
时间复杂度分析
- 预处理阶段:计算总需求 \(\text{total}\) 需要遍历数组 P,时间复杂度为 \(O(N)\)。
- 二分查找阶段:每次检查可行性需遍历数组 P,时间复杂度为 \(O(N)\)。二分查找的次数为 \(O(\log \text{max})\),其中 \(\text{max}\) 为供餐速度的上界。
- 总体时间复杂度:\(O(N \log \text{max})\)。
- 空间复杂度:仅需常数额外空间,即 \(O(1)\)。
参考代码
function solution() {const N = parseInt(readline());const M = parseInt(readline());const nums = readline().split(' ').map(Number);let max = Math.max(nums.reduce((acc, cur)=>acc+cur) - M, 0);if (max === 0) return 0;const canSatisfy = function(n) {let left = M;for (let num of nums) {if (left < num) return false;left -= num;left += n;}return true;};let l = 1, r = max, mid;while (l < r) {mid = l + Math.floor((r - l)/2);if (canSatisfy(mid)) {r = mid;} else {l = mid + 1;} }console.log(r);
}const cases = [`3
14
10 4 5`,
];let caseIndex = 0;
let lineIndex = 0;const readline = (function () {let lines = [];return function () {if (lineIndex === 0) {lines = cases[caseIndex].trim().split("\n").map((line) => line.trim());}return lines[lineIndex++];};
})();cases.forEach((_, i) => {caseIndex = i;lineIndex = 0;solution();
});