1606. Find Servers That Handled Most Number of Requests
Problem Description
You have k
servers (numbered 0 to k-1) that handle requests. Each server can only process one request at a time, though they have unlimited computational power otherwise.
When the i
-th request arrives, it gets assigned to a server following these rules:
- If all servers are busy, the request is dropped (not handled)
- If the server at position
(i % k)
is available, assign the request to that server - If that preferred server is busy, check the next server
(i+1) % k
, then(i+2) % k
, and so on, wrapping around if needed, until finding an available server
You're given two arrays:
arrival[i]
: the arrival time of thei
-th request (strictly increasing)load[i]
: how long thei
-th request takes to complete
A server becomes free again at time arrival[i] + load[i]
after starting to handle request i
.
Your task is to find which server(s) handled the most requests successfully. Return a list of server IDs that handled the maximum number of requests. The order of IDs in the output doesn't matter.
For example, if servers 0 and 2 each handled 5 requests while all other servers handled fewer, you would return [0, 2]
or [2, 0]
.
Intuition
The key insight is that we need to efficiently track which servers are free and which are busy at any given time. When a new request arrives, we need to:
- First, free up any servers that have finished their work (their end time is <= current arrival time)
- Then find the next available server starting from position
i % k
The challenge is efficiently finding "the next available server starting from position i % k
" when servers can be scattered as available or busy.
We can think of this as maintaining two groups:
- Free servers: We need to quickly find the first server >=
i % k
, or wrap around to find the smallest server if no such server exists - Busy servers: We need to know when each becomes free, so we can move them back to the free group
For free servers, we need a data structure that:
- Maintains sorted order (to find the next server >= some value)
- Supports binary search (to quickly locate position
i % k
) - Allows insertion and deletion
This points us toward using a sorted list or balanced tree structure for free servers.
For busy servers, we need to know which ones finish first. Since we only care about freeing servers that have finished by the current arrival time, we can use a min-heap ordered by end time. This way, we can efficiently pop all servers that should be freed.
The wrap-around logic becomes elegant with binary search: if we can't find any server >= i % k
(meaning our search position is at the end of the sorted list), we simply take the first server in the list (index 0), which effectively wraps around.
By maintaining these two data structures and transferring servers between them as they become busy or free, we can efficiently simulate the entire process while counting how many requests each server handles.
Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The solution uses two main data structures to track server states:
free
: ASortedList
containing IDs of available servers, maintained in sorted orderbusy
: A min-heap containing tuples of(end_time, server_id)
for busy servers
We also maintain a cnt
array to track how many requests each server has handled.
Step-by-step implementation:
-
Initialize data structures:
free = SortedList(range(k))
- Initially all servers 0 to k-1 are freebusy = []
- Empty heap for busy serverscnt = [0] * k
- Counter array initialized to zeros
-
Process each request at time
start
with durationt
:a. Free up finished servers:
while busy and busy[0][0] <= start: free.add(busy[0][1]) heappop(busy)
Pop all servers from the heap whose end time <= current start time and add them back to the free list.
b. Check if any servers are available:
if not free: continue
If no servers are free, drop this request and continue to the next one.
c. Find the appropriate server using the assignment algorithm:
j = free.bisect_left(i % k) if j == len(free): j = 0 server = free[j]
- Use binary search to find the first server >=
i % k
- If no such server exists (j equals the length of free list), wrap around by setting
j = 0
- This elegantly handles the circular search pattern
d. Assign the request to the selected server:
cnt[server] += 1 heappush(busy, (start + t, server)) free.remove(server)
- Increment the request count for this server
- Add the server to busy heap with its end time (
start + t
) - Remove the server from the free list
- Use binary search to find the first server >=
-
Find the busiest servers:
mx = max(cnt) return [i for i, v in enumerate(cnt) if v == mx]
Find the maximum count and return all server IDs that have this maximum count.
Time Complexity: O(n log k) where n is the number of requests, as each request involves heap and sorted list operations that take O(log k) time.
Space Complexity: O(k) for storing the server states and counts.
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 small example with k = 3
servers and the following requests:
arrival = [1, 2, 3, 4, 8]
load = [5, 2, 3, 3, 3]
Initial State:
free = [0, 1, 2]
(all servers available)busy = []
(no busy servers)cnt = [0, 0, 0]
(no requests handled yet)
Request 0 (i=0, arrival=1, load=5):
- Free up servers: None busy yet
- Check availability: free list has servers
- Find server:
0 % 3 = 0
, bisect_left(0) finds server 0 at position 0 - Assign to server 0:
cnt = [1, 0, 0]
busy = [(6, 0)]
(server 0 busy until time 6)free = [1, 2]
Request 1 (i=1, arrival=2, load=2):
- Free up servers: Server 0's end time is 6 > 2, so none freed
- Check availability: free list has servers
- Find server:
1 % 3 = 1
, bisect_left(1) finds server 1 at position 0 - Assign to server 1:
cnt = [1, 1, 0]
busy = [(4, 1), (6, 0)]
(heap ordered by end time)free = [2]
Request 2 (i=2, arrival=3, load=3):
- Free up servers: Earliest end time is 4 > 3, so none freed
- Check availability: free list has servers
- Find server:
2 % 3 = 2
, bisect_left(2) finds server 2 at position 0 - Assign to server 2:
cnt = [1, 1, 1]
busy = [(4, 1), (6, 0), (6, 2)]
free = []
Request 3 (i=3, arrival=4, load=3):
- Free up servers: Server 1 ends at time 4, so it becomes free
- Pop (4, 1) from busy heap
- Add server 1 to free:
free = [1]
busy = [(6, 0), (6, 2)]
- Check availability: free list has servers
- Find server:
3 % 3 = 0
, bisect_left(0) in[1]
returns position 0- Since we need server >= 0, and only server 1 is available
- bisect_left returns position 1 (end of list)
- Wrap around:
j = 0
, so we select server 1
- Assign to server 1:
cnt = [1, 2, 1]
busy = [(6, 0), (6, 2), (7, 1)]
free = []
Request 4 (i=4, arrival=8, load=3):
- Free up servers: All servers have end time <= 8
- Pop (6, 0), add server 0 to free
- Pop (6, 2), add server 2 to free
- Pop (7, 1), add server 1 to free
free = [0, 1, 2]
(sorted)busy = []
- Check availability: free list has servers
- Find server:
4 % 3 = 1
, bisect_left(1) finds server 1 at position 1 - Assign to server 1:
cnt = [1, 3, 1]
busy = [(11, 1)]
free = [0, 2]
Final Result:
cnt = [1, 3, 1]
- Maximum count is 3
- Return
[1]
(server 1 handled the most requests)
This example demonstrates:
- How servers are freed when their end time is reached
- The circular assignment pattern with wrap-around (Request 3)
- How the bisect_left operation efficiently finds the next available server
- The coordination between the free list and busy heap to track server states
Solution Implementation
1class Solution:
2 def busiestServers(self, k: int, arrival: List[int], load: List[int]) -> List[int]:
3 # Initialize a sorted list of available servers (0 to k-1)
4 available_servers = SortedList(range(k))
5
6 # Min heap to track busy servers: (end_time, server_id)
7 busy_servers = []
8
9 # Counter array to track number of requests handled by each server
10 request_count = [0] * k
11
12 # Process each request
13 for request_id, (arrival_time, processing_time) in enumerate(zip(arrival, load)):
14 # Free up servers that have completed their tasks by current arrival time
15 while busy_servers and busy_servers[0][0] <= arrival_time:
16 end_time, server_id = heappop(busy_servers)
17 available_servers.add(server_id)
18
19 # Skip this request if no servers are available
20 if not available_servers:
21 continue
22
23 # Find the next available server using round-robin strategy
24 # Start looking from server (request_id % k)
25 target_server = request_id % k
26 server_index = available_servers.bisect_left(target_server)
27
28 # If no server >= target_server exists, wrap around to the first server
29 if server_index == len(available_servers):
30 server_index = 0
31
32 # Assign the request to the selected server
33 selected_server = available_servers[server_index]
34 request_count[selected_server] += 1
35
36 # Mark server as busy until arrival_time + processing_time
37 heappush(busy_servers, (arrival_time + processing_time, selected_server))
38 available_servers.remove(selected_server)
39
40 # Find servers with maximum request count
41 max_requests = max(request_count)
42 return [server_id for server_id, count in enumerate(request_count) if count == max_requests]
43
1class Solution {
2 public List<Integer> busiestServers(int k, int[] arrival, int[] load) {
3 // Array to track the number of requests handled by each server
4 int[] requestCount = new int[k];
5
6 // Min heap to store busy servers, sorted by their end time
7 // Each element is [endTime, serverIndex]
8 PriorityQueue<int[]> busyServers = new PriorityQueue<>(
9 Comparator.comparingInt(a -> a[0])
10 );
11
12 // TreeSet to maintain available servers in sorted order
13 TreeSet<Integer> availableServers = new TreeSet<>();
14
15 // Initialize all servers as available
16 for (int i = 0; i < k; i++) {
17 availableServers.add(i);
18 }
19
20 // Process each incoming request
21 for (int i = 0; i < arrival.length; i++) {
22 int requestArrivalTime = arrival[i];
23 int requestEndTime = requestArrivalTime + load[i];
24
25 // Free up servers that have completed their tasks before current request arrives
26 while (!busyServers.isEmpty() && busyServers.peek()[0] <= requestArrivalTime) {
27 int[] completedServer = busyServers.poll();
28 availableServers.add(completedServer[1]);
29 }
30
31 // Skip this request if no servers are available
32 if (availableServers.isEmpty()) {
33 continue;
34 }
35
36 // Find the target server using round-robin with preference for server (i % k)
37 // First try to find a server with index >= (i % k)
38 Integer targetServer = availableServers.ceiling(i % k);
39
40 // If no such server exists, wrap around and take the first available server
41 if (targetServer == null) {
42 targetServer = availableServers.first();
43 }
44
45 // Assign the request to the target server
46 requestCount[targetServer]++;
47 busyServers.offer(new int[] {requestEndTime, targetServer});
48 availableServers.remove(targetServer);
49 }
50
51 // Find the maximum number of requests handled by any server
52 int maxRequests = 0;
53 for (int count : requestCount) {
54 maxRequests = Math.max(maxRequests, count);
55 }
56
57 // Collect all servers that handled the maximum number of requests
58 List<Integer> busiestServersList = new ArrayList<>();
59 for (int i = 0; i < k; i++) {
60 if (requestCount[i] == maxRequests) {
61 busiestServersList.add(i);
62 }
63 }
64
65 return busiestServersList;
66 }
67}
68
1class Solution {
2public:
3 vector<int> busiestServers(int k, vector<int>& arrival, vector<int>& load) {
4 // Set to track available servers
5 set<int> availableServers;
6 for (int i = 0; i < k; ++i) {
7 availableServers.insert(i);
8 }
9
10 // Min heap to track busy servers (sorted by end time)
11 // Pair: {end_time, server_id}
12 priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> busyServers;
13
14 // Count of requests handled by each server
15 vector<int> requestCount(k);
16
17 // Process each incoming request
18 for (int i = 0; i < arrival.size(); ++i) {
19 int startTime = arrival[i];
20 int endTime = startTime + load[i];
21
22 // Free up servers that have completed their tasks
23 while (!busyServers.empty() && busyServers.top().first <= startTime) {
24 availableServers.insert(busyServers.top().second);
25 busyServers.pop();
26 }
27
28 // Skip this request if no servers are available
29 if (availableServers.empty()) {
30 continue;
31 }
32
33 // Find the next available server using round-robin starting from (i % k)
34 auto iterator = availableServers.lower_bound(i % k);
35 if (iterator == availableServers.end()) {
36 // Wrap around to the beginning if no server >= (i % k) is found
37 iterator = availableServers.begin();
38 }
39
40 // Assign the request to the selected server
41 int selectedServer = *iterator;
42 ++requestCount[selectedServer];
43 busyServers.emplace(endTime, selectedServer);
44 availableServers.erase(selectedServer);
45 }
46
47 // Find the maximum number of requests handled
48 int maxRequests = *max_element(requestCount.begin(), requestCount.end());
49
50 // Collect all servers that handled the maximum number of requests
51 vector<int> result;
52 for (int i = 0; i < k; ++i) {
53 if (requestCount[i] == maxRequests) {
54 result.push_back(i);
55 }
56 }
57
58 return result;
59 }
60};
61
1function busiestServers(k: number, arrival: number[], load: number[]): number[] {
2 // Set to track available servers
3 const availableServers = new Set<number>();
4 for (let i = 0; i < k; i++) {
5 availableServers.add(i);
6 }
7
8 // Min heap to track busy servers (sorted by end time)
9 // Array of tuples: [endTime, serverId]
10 const busyServers: [number, number][] = [];
11
12 // Count of requests handled by each server
13 const requestCount: number[] = new Array(k).fill(0);
14
15 // Process each incoming request
16 for (let i = 0; i < arrival.length; i++) {
17 const startTime = arrival[i];
18 const endTime = startTime + load[i];
19
20 // Free up servers that have completed their tasks
21 while (busyServers.length > 0 && busyServers[0][0] <= startTime) {
22 const [, serverId] = busyServers.shift()!;
23 availableServers.add(serverId);
24 }
25
26 // Skip this request if no servers are available
27 if (availableServers.size === 0) {
28 continue;
29 }
30
31 // Find the next available server using round-robin starting from (i % k)
32 const targetServer = i % k;
33 let selectedServer: number | undefined;
34
35 // Convert set to sorted array for searching
36 const sortedAvailable = Array.from(availableServers).sort((a, b) => a - b);
37
38 // Find the first server >= targetServer
39 const index = sortedAvailable.findIndex(server => server >= targetServer);
40
41 if (index !== -1) {
42 // Found a server >= targetServer
43 selectedServer = sortedAvailable[index];
44 } else {
45 // Wrap around to the beginning if no server >= targetServer is found
46 selectedServer = sortedAvailable[0];
47 }
48
49 // Assign the request to the selected server
50 requestCount[selectedServer]++;
51
52 // Add to busy servers heap
53 busyServers.push([endTime, selectedServer]);
54 // Keep the heap sorted by end time
55 busyServers.sort((a, b) => a[0] - b[0]);
56
57 // Remove server from available set
58 availableServers.delete(selectedServer);
59 }
60
61 // Find the maximum number of requests handled
62 const maxRequests = Math.max(...requestCount);
63
64 // Collect all servers that handled the maximum number of requests
65 const result: number[] = [];
66 for (let i = 0; i < k; i++) {
67 if (requestCount[i] === maxRequests) {
68 result.push(i);
69 }
70 }
71
72 return result;
73}
74
Time and Space Complexity
Time Complexity: O(n * (log k + k))
where n
is the number of requests and k
is the number of servers.
- The main loop iterates through all
n
requests - For each request:
- The while loop for freeing servers: In the worst case, we might pop all
k
servers from the heap, each pop operation takesO(log k)
, and each add to SortedList takesO(log k)
. This givesO(k * log k)
in the worst case bisect_left
operation on SortedList:O(log k)
heappush
operation:O(log k)
remove
operation on SortedList:O(k)
in the worst case (as it needs to find and remove the element)
- The while loop for freeing servers: In the worst case, we might pop all
- The final max operation:
O(k)
- The list comprehension:
O(k)
The dominant operation in each iteration is the remove
operation which is O(k)
, making the overall time complexity O(n * k)
. However, considering all operations more precisely, it's O(n * (log k + k))
which simplifies to O(n * k)
.
Space Complexity: O(k)
free
SortedList: stores at mostk
servers, soO(k)
busy
heap: stores at mostk
servers (each server can only be busy once at a time), soO(k)
cnt
array: stores exactlyk
counters, soO(k)
- The final result list: at most
k
servers, soO(k)
The overall space complexity is O(k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Server Selection After Wrap-Around
The Pitfall: A common mistake is incorrectly implementing the circular search for the next available server. Many developers might try something like:
# INCORRECT approach
target = request_id % k
for offset in range(k):
server = (target + offset) % k
if server in available_servers:
selected_server = server
break
This linear search approach is inefficient (O(k) per request) and doesn't leverage the sorted nature of available servers.
Why It's Wrong:
- Time complexity degrades to O(n*k) for n requests
- Doesn't utilize the efficiency of binary search
- Can timeout on large inputs
The Correct Solution:
target_server = request_id % k
server_index = available_servers.bisect_left(target_server)
# Critical: Handle wrap-around by resetting to 0
if server_index == len(available_servers):
server_index = 0
selected_server = available_servers[server_index]
This uses binary search (O(log k)) and elegantly handles wrap-around by checking if the index equals the list length.
2. Not Freeing All Completed Servers
The Pitfall: Using if
instead of while
when freeing servers:
# INCORRECT - only frees one server if busy_servers and busy_servers[0][0] <= arrival_time: end_time, server_id = heappop(busy_servers) available_servers.add(server_id)
Why It's Wrong: Multiple servers might finish at or before the current arrival time. Using if
only frees one server, leaving others incorrectly marked as busy.
The Correct Solution:
# Correct - frees ALL completed servers while busy_servers and busy_servers[0][0] <= arrival_time: end_time, server_id = heappop(busy_servers) available_servers.add(server_id)
3. Confusion Between Request Index and Server Index
The Pitfall: Mixing up i
(request index) with server IDs:
# INCORRECT - using wrong variable target_server = i % k # Should be request_id or index of current request
Why It's Wrong: The formula (i % k)
determines the preferred server based on the request's position in the sequence, not based on any server property. Using the wrong variable breaks the round-robin assignment logic.
The Correct Solution: Ensure you're using the request index (position in the arrival array) for the modulo operation:
for request_id, (arrival_time, processing_time) in enumerate(zip(arrival, load)):
# ...
target_server = request_id % k # Correct: using request index
4. Forgetting to Remove Server from Available List
The Pitfall: After assigning a request to a server, forgetting to remove it from the available list:
# INCORRECT - server remains in available list selected_server = available_servers[server_index] request_count[selected_server] += 1 heappush(busy_servers, (arrival_time + processing_time, selected_server)) # Missing: available_servers.remove(selected_server)
Why It's Wrong: The server would appear available for subsequent requests even though it's busy, leading to incorrect request assignments and counts.
The Correct Solution: Always remove the server from available list after assignment:
selected_server = available_servers[server_index] request_count[selected_server] += 1 heappush(busy_servers, (arrival_time + processing_time, selected_server)) available_servers.remove(selected_server) # Critical step
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!