【每日一题】LeetCode 2398.预算内的最多机器人数目(滑动窗口、数组、二分查找、前缀和、堆(优先队列))
【每日一题】LeetCode 2398.预算内的最多机器人数目(滑动窗口、数组、二分查找、前缀和、堆(优先队列))
题目描述
给定两个整数数组 chargeTimes
和 runningCosts
,分别代表 n 个机器人的充电时间和运行成本。再给定一个整数 budget
,表示预算。我们需要计算在不超过预算的情况下,最多可以连续运行多少个机器人。
运行 k 个机器人的总开销计算公式为:max(chargeTimes) + k * sum(runningCosts)
,其中 max(chargeTimes)
是这 k 个机器人中最大充电时间,sum(runningCosts)
是这 k 个机器人的运行时间之和。
思路分析
这个问题可以通过滑动窗口的方法来解决。我们的目标是在不超过预算的情况下,找到能够连续运行的最多机器人数量。
-
初始化两个指针
left
和right
分别指向数组的起始位置,以及一些必要的变量,如maxRobots
用于存储最大机器人数量,maxChargeTimes
用于存储窗口内的最大充电时间,sum
用于存储窗口内运行成本的总和。 -
使用
right
指针扩展窗口,将right
指针所指的机器人加入到当前窗口中,更新maxChargeTimes
和sum
。 -
检查当前窗口的总开销是否超过预算。如果超过预算,通过移动
left
指针缩小窗口,直到窗口内的总开销不超过预算。 -
在每一步中,更新
maxRobots
,记录下当前窗口内能够运行的最大机器人数量。 -
重复步骤 2-4,直到
right
指针遍历完整个数组。 -
返回
maxRobots
作为最终结果。
输入示例
-
示例 1:
- 输入:
chargeTimes = [3,6,1,3,4]
,runningCosts = [2,1,3,4,5]
,budget = 25
- 输出:
3
- 解释:可以在 budget 以内运行所有单个机器人或者连续运行 2 个机器人。选择前 3 个机器人,可以得到答案最大值 3 。总开销是 max(3,6,1) + 3 * sum(2,1,3) = 6 + 3 * 6 = 24 ,小于 25 。可以看出无法在 budget 以内连续运行超过 3 个机器人,所以我们返回 3 。
- 输入:
-
示例 2:
- 输入:
chargeTimes = [11,12,19]
,runningCosts = [10,8,7]
,budget = 19
- 输出:
0
- 解释:即使运行任何一个单个机器人,还是会超出 budget,所以我们返回 0 。
- 输入:
代码实现
class Solution {
public int maximumRobots(int[] chargeTimes, int[] runningCosts, long budget) {
int n = chargeTimes.length;
int maxRobots = 0; // 最多可以连续运行的机器人数目
int maxChargeTimes = 0; // 当前窗口内的最大充电时间
long sum = 0; // 当前窗口内的运行成本总和
int left = 0; // 滑动窗口的左边界
int right = 0; // 滑动窗口的右边界
while (right < n) {
// 扩展窗口,将当前机器人加入窗口
maxChargeTimes = Math.max(maxChargeTimes, chargeTimes[right]);
sum += runningCosts[right];
// 当前窗口的总开销 = 最大充电时间 + 当前窗口内机器人数量 * 运行成本总和
while (maxChargeTimes + (right - left + 1) * sum > budget) {
// 如果当前窗口的总开销超过预算,缩小窗口
sum -= runningCosts[left];
left++;
// 重新计算窗口内的最大充电时间
maxChargeTimes = 0;
for (int i = left; i <= right; i++) {
maxChargeTimes = Math.max(maxChargeTimes, chargeTimes[i]);
}
}
// 更新最大机器人数量
maxRobots = Math.max(maxRobots, right - left + 1);
right++;
}
return maxRobots;
}
}
优化思路分析
使用双端队列(Deque
)来维护一个单调递减的充电时间序列。这种方法可以更高效地处理窗口的扩展和收缩,从而减少不必要的计算。
-
维护单调队列:使用一个双端队列
q
来维护当前窗口内机器人的索引,这个队列按照机器人的充电时间从大到小的顺序排列。这样可以快速找到当前窗口内最大充电时间的机器人。 -
扩展窗口:遍历每个机器人,将其索引加入到队列中。如果队列尾部的机器人充电时间小于当前机器人的充电时间,则将其移除,因为当前机器人的充电时间更大,可以替换掉队列尾部的机器人。
-
计算运行成本总和:随着窗口的扩展,累加当前窗口内所有机器人的运行成本。
-
收缩窗口:如果当前窗口的总开销超过了预算,通过移除窗口左侧的机器人来缩小窗口,直到总开销不超过预算。同时,更新运行成本总和。
-
更新最大机器人数量:在每一步中,更新最大机器人数量
ans
,记录下当前窗口内能够运行的最大机器人数量。 -
返回结果:遍历完成后,返回记录的最大机器人数量。
优化后的代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int maximumRobots(int[] chargeTimes, int[] runningCosts, long budget) {
int ans = 0; // 记录最大机器人数量
int left = 0; // 滑动窗口的左边界
long sum = 0; // 当前窗口内的运行成本总和
Deque<Integer> q = new ArrayDeque<>(); // 用于维护单调递减的充电时间序列
for (int right = 0; right < chargeTimes.length; right++) {
// 1. 扩展窗口
while (!q.isEmpty() && chargeTimes[right] >= chargeTimes[q.peekLast()]) {
q.pollLast(); // 移除队列尾部充电时间较小的机器人
}
q.addLast(right); // 将当前机器人加入队列
sum += runningCosts[right]; // 更新运行成本总和
// 2. 收缩窗口
while (!q.isEmpty() && chargeTimes[q.peekFirst()] + (right - left + 1) * sum > budget) {
if (q.peekFirst() == left) {
q.pollFirst(); // 移除队列头部的机器人
}
sum -= runningCosts[left++]; // 更新运行成本总和
}
// 3. 更新答案
ans = Math.max(ans, right - left + 1); // 更新最大机器人数量
}
return ans; // 返回最终结果
}
}