2398. Maximum Number of Robots Within Budget
Problem Description
You have n
robots, each with specific charging and running costs. Given two arrays:
chargeTimes[i]
: the cost to charge the i-th robotrunningCosts[i]
: the cost to run the i-th robot
You need to select k
consecutive robots (a contiguous subarray) to run within a given budget
.
The total cost formula for running k
chosen consecutive robots is:
Total Cost = max(chargeTimes) + k * sum(runningCosts)
Where:
max(chargeTimes)
is the highest charge cost among the selectedk
robotssum(runningCosts)
is the sum of all running costs for the selectedk
robotsk
is the number of consecutive robots chosen
Your task is to find the maximum number of consecutive robots you can run without exceeding the budget.
For example, if you select robots from index i
to index j
, you're selecting j - i + 1
consecutive robots. The cost would be the maximum charge time among these robots plus the count of robots multiplied by the sum of their running costs.
The goal is to maximize the number of consecutive robots while keeping the total cost ≤ budget
.
Intuition
The key insight is recognizing this as a sliding window problem with a twist - we need to track the maximum value within each window.
Think about it this way: we want to find the longest consecutive subarray of robots that fits our budget. This naturally suggests trying different window sizes. We could check every possible consecutive subarray, but that would be inefficient.
Instead, we can use a sliding window approach where we expand the window by adding robots on the right and shrink it by removing robots from the left when the cost exceeds the budget. The challenge is efficiently tracking the maximum chargeTime
in our current window as it changes.
For tracking the maximum in a sliding window, a monotonic deque is perfect. Why? Because:
- When a new element comes in that's larger than elements at the back of the deque, those smaller elements will never be the maximum as long as this new element is in the window
- The front of the deque always contains the maximum element in the current window
- When the window shrinks and removes the leftmost element, we can easily remove it from the deque if it's at the front
The two-pointer technique works naturally here:
- Right pointer (
r
) expands the window by including new robots - Left pointer (
l
) shrinks the window when the total cost exceeds budget - We continuously track the running sum of
runningCosts
for efficiency - The monotonic deque maintains the maximum
chargeTime
in the current window
The formula (r - l + 1) * s + chargeTimes[q[0]] > budget
checks if our current window exceeds the budget, where (r - l + 1)
is the window size, s
is the sum of running costs, and chargeTimes[q[0]]
is the maximum charge time.
This approach efficiently finds the maximum window size in a single pass through the array, giving us an O(n)
solution.
Learn more about Queue, Binary Search, Prefix Sum, Sliding Window, Monotonic Queue and Heap (Priority Queue) patterns.
Solution Approach
The solution uses a sliding window technique combined with a monotonic deque to efficiently track the maximum charge time within the current window.
Data Structures Used:
- A deque
q
to maintain indices of robots in decreasing order of their charge times - Two pointers
l
andr
for the sliding window boundaries - A variable
s
to track the sum of running costs in the current window
Implementation Walkthrough:
-
Initialize variables:
q = deque()
- monotonic deque for tracking maximum charge timeans = 0
- stores the maximum number of consecutive robotss = 0
- running sum ofrunningCosts
in current windowl = 0
- left boundary of the sliding window
-
Expand the window (right pointer):
for r, (t, c) in enumerate(zip(chargeTimes, runningCosts)): s += c # Add current robot's running cost to sum
-
Maintain the monotonic deque:
while q and chargeTimes[q[-1]] <= t: q.pop() q.append(r)
- Remove indices from the back of deque if their charge times are less than or equal to the current robot's charge time
- This ensures the deque front always has the index of the robot with maximum charge time in the current window
-
Shrink the window when budget is exceeded:
while q and (r - l + 1) * s + chargeTimes[q[0]] > budget: if q[0] == l: q.popleft() # Remove from deque if it's going out of window s -= runningCosts[l] # Subtract the running cost of removed robot l += 1 # Move left boundary right
- Check if total cost exceeds budget:
window_size * sum_of_running_costs + max_charge_time
- If the robot at index
l
is the maximum (at deque front), remove it from deque - Update the running sum and move the left pointer
- Check if total cost exceeds budget:
-
Update the answer:
ans = max(ans, r - l + 1)
- After adjusting the window to fit the budget, record the window size
The algorithm processes each robot exactly once with the right pointer and at most once with the left pointer, resulting in O(n)
time complexity. The space complexity is O(n)
for the deque in the worst case.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Given:
chargeTimes = [3, 6, 1, 3, 4]
runningCosts = [2, 1, 3, 4, 5]
budget = 25
We want to find the maximum number of consecutive robots we can run.
Step-by-step execution:
Initial state: l = 0
, r = 0
, s = 0
, q = []
, ans = 0
Iteration 1 (r = 0):
- Add robot 0:
s = 0 + 2 = 2
- Deque is empty, add index 0:
q = [0]
- Check cost:
(0-0+1) * 2 + 3 = 1 * 2 + 3 = 5 ≤ 25
✓ - Update answer:
ans = max(0, 1) = 1
Iteration 2 (r = 1):
- Add robot 1:
s = 2 + 1 = 3
chargeTimes[1] = 6 > chargeTimes[0] = 3
, add to deque:q = [0, 1]
- But wait, we maintain decreasing order, so we first remove 0:
q = [1]
- Check cost:
(1-0+1) * 3 + 6 = 2 * 3 + 6 = 12 ≤ 25
✓ - Update answer:
ans = max(1, 2) = 2
Iteration 3 (r = 2):
- Add robot 2:
s = 3 + 3 = 6
chargeTimes[2] = 1 < chargeTimes[1] = 6
, add to deque:q = [1, 2]
- Check cost:
(2-0+1) * 6 + 6 = 3 * 6 + 6 = 24 ≤ 25
✓ - Update answer:
ans = max(2, 3) = 3
Iteration 4 (r = 3):
- Add robot 3:
s = 6 + 4 = 10
chargeTimes[3] = 3 > chargeTimes[2] = 1
, remove 2:q = [1, 3]
- Check cost:
(3-0+1) * 10 + 6 = 4 * 10 + 6 = 46 > 25
✗ - Need to shrink window:
q[0] = 1 ≠ l = 0
, so don't remove from deque- Remove robot 0:
s = 10 - 2 = 8
,l = 1
- Check cost:
(3-1+1) * 8 + 6 = 3 * 8 + 6 = 30 > 25
✗ - Continue shrinking:
q[0] = 1 = l = 1
, remove from deque:q = [3]
- Remove robot 1:
s = 8 - 1 = 7
,l = 2
- Check cost:
(3-2+1) * 7 + 3 = 2 * 7 + 3 = 17 ≤ 25
✓
- Update answer:
ans = max(3, 2) = 3
Iteration 5 (r = 4):
- Add robot 4:
s = 7 + 5 = 12
chargeTimes[4] = 4 > chargeTimes[3] = 3
, remove 3:q = [4]
- Check cost:
(4-2+1) * 12 + 4 = 3 * 12 + 4 = 40 > 25
✗ - Need to shrink window:
q[0] = 4 ≠ l = 2
, so don't remove from deque- Remove robot 2:
s = 12 - 3 = 9
,l = 3
- Check cost:
(4-3+1) * 9 + 4 = 2 * 9 + 4 = 22 ≤ 25
✓
- Update answer:
ans = max(3, 2) = 3
Final answer: 3 (we can run at most 3 consecutive robots within the budget)
The optimal selection is robots [0, 1, 2] with total cost = max(3, 6, 1) + 3 * (2 + 1 + 3) = 6 + 18 = 24 ≤ 25.
Solution Implementation
1from collections import deque
2from typing import List
3
4
5class Solution:
6 def maximumRobots(
7 self, chargeTimes: List[int], runningCosts: List[int], budget: int
8 ) -> int:
9 # Monotonic deque to maintain indices of charge times in descending order
10 max_charge_deque = deque()
11
12 # Variables to track the maximum number of robots and running sum
13 max_robots = 0
14 running_sum = 0
15 left = 0
16
17 # Iterate through each robot using sliding window approach
18 for right, (charge_time, running_cost) in enumerate(zip(chargeTimes, runningCosts)):
19 # Add current robot's running cost to the sum
20 running_sum += running_cost
21
22 # Maintain monotonic deque: remove elements smaller than current charge time
23 while max_charge_deque and chargeTimes[max_charge_deque[-1]] <= charge_time:
24 max_charge_deque.pop()
25
26 # Add current index to deque
27 max_charge_deque.append(right)
28
29 # Shrink window from left while total cost exceeds budget
30 # Total cost = max(chargeTimes in window) + k * sum(runningCosts in window)
31 # where k is the number of robots (window size)
32 while max_charge_deque and (right - left + 1) * running_sum + chargeTimes[max_charge_deque[0]] > budget:
33 # Remove leftmost element from deque if it's going out of window
34 if max_charge_deque[0] == left:
35 max_charge_deque.popleft()
36
37 # Remove leftmost robot's running cost from sum
38 running_sum -= runningCosts[left]
39
40 # Move left pointer forward
41 left += 1
42
43 # Update maximum number of robots that can be used
44 max_robots = max(max_robots, right - left + 1)
45
46 return max_robots
47
1class Solution {
2 public int maximumRobots(int[] chargeTimes, int[] runningCosts, long budget) {
3 // Monotonic deque to maintain indices of charge times in descending order
4 // The front always contains the index of the maximum charge time in current window
5 Deque<Integer> maxChargeIndices = new ArrayDeque<>();
6
7 int n = chargeTimes.length;
8 int maxRobots = 0;
9 long runningCostSum = 0;
10
11 // Sliding window approach with two pointers
12 int left = 0;
13 for (int right = 0; right < n; right++) {
14 // Add current robot's running cost to the sum
15 runningCostSum += runningCosts[right];
16
17 // Maintain monotonic deque property: remove smaller elements from the back
18 // This ensures the deque front always has the maximum charge time
19 while (!maxChargeIndices.isEmpty() &&
20 chargeTimes[maxChargeIndices.peekLast()] <= chargeTimes[right]) {
21 maxChargeIndices.pollLast();
22 }
23 maxChargeIndices.offerLast(right);
24
25 // Shrink window from left while the cost exceeds budget
26 // Cost formula: (number of robots) * (sum of running costs) + (max charge time)
27 while (!maxChargeIndices.isEmpty() &&
28 (right - left + 1) * runningCostSum + chargeTimes[maxChargeIndices.peekFirst()] > budget) {
29
30 // Remove the leftmost index from deque if it's going out of window
31 if (maxChargeIndices.peekFirst() == left) {
32 maxChargeIndices.pollFirst();
33 }
34
35 // Remove leftmost robot from window and adjust running cost sum
36 runningCostSum -= runningCosts[left];
37 left++;
38 }
39
40 // Update maximum number of robots that can be used within budget
41 maxRobots = Math.max(maxRobots, right - left + 1);
42 }
43
44 return maxRobots;
45 }
46}
47
1class Solution {
2public:
3 int maximumRobots(vector<int>& chargeTimes, vector<int>& runningCosts, long long budget) {
4 // Monotonic deque to maintain maximum charge time in current window
5 // Stores indices in decreasing order of charge times
6 deque<int> maxChargeDeque;
7
8 // Running sum of running costs in current window
9 long long runningCostSum = 0;
10
11 // Maximum number of consecutive robots that can be used
12 int maxRobots = 0;
13
14 int n = chargeTimes.size();
15
16 // Sliding window approach with two pointers
17 int left = 0;
18 for (int right = 0; right < n; ++right) {
19 // Add current robot's running cost to the sum
20 runningCostSum += runningCosts[right];
21
22 // Maintain monotonic decreasing deque for maximum charge time
23 // Remove all elements smaller than or equal to current element from back
24 while (!maxChargeDeque.empty() &&
25 chargeTimes[maxChargeDeque.back()] <= chargeTimes[right]) {
26 maxChargeDeque.pop_back();
27 }
28 maxChargeDeque.push_back(right);
29
30 // Shrink window from left while total cost exceeds budget
31 // Total cost = max(chargeTimes) + k * sum(runningCosts), where k is window size
32 while (!maxChargeDeque.empty() &&
33 (right - left + 1) * runningCostSum + chargeTimes[maxChargeDeque.front()] > budget) {
34
35 // Remove leftmost element from deque if it's going out of window
36 if (maxChargeDeque.front() == left) {
37 maxChargeDeque.pop_front();
38 }
39
40 // Remove leftmost robot's running cost and move left pointer
41 runningCostSum -= runningCosts[left];
42 left++;
43 }
44
45 // Update maximum number of robots that can be used
46 maxRobots = max(maxRobots, right - left + 1);
47 }
48
49 return maxRobots;
50 }
51};
52
1// Global node type for deque implementation
2interface DequeNode<T> {
3 value: T;
4 next: DequeNode<T> | null;
5 prev: DequeNode<T> | null;
6}
7
8// Global deque state variables
9let dequeFront: DequeNode<number> | null = null;
10let dequeBack: DequeNode<number> | null = null;
11let dequeSize: number = 0;
12
13// Check if deque is empty
14function isEmpty(): boolean {
15 return dequeSize === 0;
16}
17
18// Add element to the front of deque
19function pushFront(val: number): void {
20 const newNode: DequeNode<number> = {
21 value: val,
22 next: null,
23 prev: null
24 };
25
26 if (isEmpty()) {
27 dequeFront = newNode;
28 dequeBack = newNode;
29 } else {
30 newNode.next = dequeFront;
31 dequeFront!.prev = newNode;
32 dequeFront = newNode;
33 }
34 dequeSize++;
35}
36
37// Add element to the back of deque
38function pushBack(val: number): void {
39 const newNode: DequeNode<number> = {
40 value: val,
41 next: null,
42 prev: null
43 };
44
45 if (isEmpty()) {
46 dequeFront = newNode;
47 dequeBack = newNode;
48 } else {
49 newNode.prev = dequeBack;
50 dequeBack!.next = newNode;
51 dequeBack = newNode;
52 }
53 dequeSize++;
54}
55
56// Remove and return element from the front of deque
57function popFront(): number | undefined {
58 if (isEmpty()) {
59 return undefined;
60 }
61
62 const value = dequeFront!.value;
63 dequeFront = dequeFront!.next;
64
65 if (dequeFront !== null) {
66 dequeFront.prev = null;
67 } else {
68 dequeBack = null;
69 }
70 dequeSize--;
71 return value;
72}
73
74// Remove and return element from the back of deque
75function popBack(): number | undefined {
76 if (isEmpty()) {
77 return undefined;
78 }
79
80 const value = dequeBack!.value;
81 dequeBack = dequeBack!.prev;
82
83 if (dequeBack !== null) {
84 dequeBack.next = null;
85 } else {
86 dequeFront = null;
87 }
88 dequeSize--;
89 return value;
90}
91
92// Get the value at the front of deque without removing it
93function frontValue(): number | undefined {
94 return dequeFront?.value;
95}
96
97// Get the value at the back of deque without removing it
98function backValue(): number | undefined {
99 return dequeBack?.value;
100}
101
102// Reset deque to initial state
103function clearDeque(): void {
104 dequeFront = null;
105 dequeBack = null;
106 dequeSize = 0;
107}
108
109/**
110 * Find the maximum number of consecutive robots that can run within budget
111 * @param chargeTimes - Array of charge times for each robot
112 * @param runningCosts - Array of running costs for each robot
113 * @param budget - Maximum budget available
114 * @returns Maximum number of consecutive robots that can run
115 */
116function maximumRobots(chargeTimes: number[], runningCosts: number[], budget: number): number {
117 // Reset deque for new function call
118 clearDeque();
119
120 const n: number = chargeTimes.length;
121 let maxRobots: number = 0;
122 let runningCostSum: number = 0;
123
124 // Use sliding window with two pointers
125 for (let left = 0, right = 0; right < n; right++) {
126 // Add current robot's running cost to sum
127 runningCostSum += runningCosts[right];
128
129 // Maintain deque as monotonic decreasing queue for max charge time
130 // Remove elements from back that are smaller than or equal to current
131 while (!isEmpty() && chargeTimes[backValue()!] <= chargeTimes[right]) {
132 popBack();
133 }
134 pushBack(right);
135
136 // Shrink window from left if total cost exceeds budget
137 // Total cost = max(chargeTimes in window) + k * sum(runningCosts in window)
138 // where k is the number of robots (window size)
139 while (!isEmpty() && (right - left + 1) * runningCostSum + chargeTimes[frontValue()!] > budget) {
140 // Remove leftmost index from deque if it's going out of window
141 if (frontValue() === left) {
142 popFront();
143 }
144 // Remove leftmost robot's running cost and move left pointer
145 runningCostSum -= runningCosts[left];
146 left++;
147 }
148
149 // Update maximum number of robots
150 maxRobots = Math.max(maxRobots, right - left + 1);
151 }
152
153 return maxRobots;
154}
155
Time and Space Complexity
Time Complexity: O(n)
The algorithm uses a sliding window approach with a deque to track maximum charge times. The outer loop iterates through all n
robots once. While there are nested while loops inside, each element can only be added to and removed from the deque at most once, and each element can only be involved in the window expansion (right pointer movement) and contraction (left pointer movement) at most once. Therefore, despite the nested structure, each operation on each element happens a constant number of times, resulting in O(n)
total time complexity.
Space Complexity: O(n)
The deque q
stores indices of robots within the current window. In the worst case, when all charge times are in increasing order and the budget allows all robots to be selected, the deque could contain all n
indices, requiring O(n)
space. Other variables (ans
, s
, l
, r
, t
, c
) use constant space O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Total Cost Calculation
Pitfall: A common mistake is misunderstanding the cost formula. Some might incorrectly calculate the total cost as:
max(chargeTimes) + sum(runningCosts)
(missing the multiplication by k)sum(chargeTimes) + k * sum(runningCosts)
(using sum instead of max for charge times)
Solution: Remember the correct formula:
total_cost = max(chargeTimes[l:r+1]) + (r - l + 1) * sum(runningCosts[l:r+1])
Where (r - l + 1)
is the window size (k), not just 1.
2. Deque Maintenance Error When Shrinking Window
Pitfall: Forgetting to remove the leftmost element from the deque when it goes out of the window bounds. This causes the deque to contain indices that are no longer in the current window, leading to incorrect maximum charge time calculations.
# Wrong: Only updating sum and left pointer while total_cost > budget: running_sum -= runningCosts[left] left += 1 # Missing: deque cleanup!
Solution: Always check if the front of the deque equals the left boundary before incrementing:
while max_charge_deque and total_cost > budget: if max_charge_deque[0] == left: # Critical check max_charge_deque.popleft() running_sum -= runningCosts[left] left += 1
3. Empty Deque Access
Pitfall: Accessing max_charge_deque[0]
when the deque might be empty, causing an IndexError. This can happen after removing elements or when no valid window exists.
Solution: Always check if the deque is non-empty before accessing:
# Check deque is not empty in the while condition while max_charge_deque and (right - left + 1) * running_sum + chargeTimes[max_charge_deque[0]] > budget: # Safe to access max_charge_deque[0] here
4. Integer Overflow in Cost Calculation
Pitfall: For large values, the expression (r - l + 1) * running_sum
might overflow in languages with fixed integer sizes. Even in Python, this could cause performance issues with very large numbers.
Solution: Consider the order of operations and potential early termination:
# If window size alone exceeds budget/min_cost, skip calculation window_size = right - left + 1 if window_size > budget: # Early check for impossible cases # Shrink window
5. Off-by-One Error in Window Size
Pitfall: Incorrectly calculating the window size as r - l
instead of r - l + 1
, leading to wrong cost calculations and potentially selecting more robots than the budget allows.
Solution: Always use right - left + 1
for the window size:
window_size = right - left + 1 # Correct # NOT: window_size = right - left # Wrong!
In a binary min heap, the maximum element can be found in:
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Want a Structured Path to Master System Design Too? Don’t Miss This!