1882. Process Tasks Using Servers
Problem Description
You have a set of servers and a list of tasks that need to be processed. Each server has a weight (represented in the servers
array), and each task requires a certain amount of time to complete (represented in the tasks
array).
The task assignment system works as follows:
-
Task Arrival: Tasks arrive sequentially - the
j-th
task arrives at secondj
(starting from second 0 for the 0-th task). -
Server Selection Rules: When assigning a task to a server:
- Choose the free server with the smallest weight
- If multiple servers have the same weight, choose the one with the smallest index
-
Queueing: If no servers are free when a task arrives, the task waits in a queue. When servers become free, tasks are assigned from the queue in the order they arrived (FIFO).
-
Server Availability: When a server starts processing task
j
at timet
, it becomes free again at timet + tasks[j]
. -
Simultaneous Freedom: If multiple servers become free at the same time, multiple queued tasks can be assigned simultaneously, following the weight and index priority rules.
Your goal is to determine which server will process each task. Return an array ans
where ans[j]
is the index of the server that processes the j-th
task.
Example scenario: If you have servers with weights [3, 3, 2]
and tasks with durations [1, 2, 3, 2, 1, 2]
:
- Task 0 (arrives at second 0) → assigned to server 2 (weight 2, smallest)
- Task 1 (arrives at second 1) → assigned to server 0 (among free servers with weight 3, smallest index)
- Task 2 (arrives at second 2) → assigned to server 1 (only free server)
- And so on...
The key challenge is managing the timing of when servers become available and efficiently selecting the optimal server based on the priority rules.
Intuition
The problem essentially requires us to simulate a task scheduling system where we need to track which servers are free and which are busy at any given time. Let's think through what we need:
-
Tracking Free Servers: We need a way to quickly find the free server with the smallest weight (and smallest index as a tiebreaker). This suggests using a min-heap where we can extract the minimum element in
O(log n)
time. -
Tracking Busy Servers: We also need to know when busy servers will become free. Another min-heap works well here, ordered by the time when each server becomes available.
The key insight is that we process tasks in chronological order (task j
arrives at second j
). At each time step, we can:
- First, check if any busy servers have become free by the current time
- Move those servers from the busy heap to the idle heap
- Then assign the current task to the best available server
The two-heap approach naturally handles the priority rules:
- The
idle
heap stores tuples(weight, index)
so Python's heap will automatically prioritize by weight first, then by index - The
busy
heap stores tuples(free_time, weight, index)
to track when each server becomes available
When no servers are idle, we have a special case: we need to "fast-forward" time to when the next server becomes free. We can do this by taking the server with the earliest free time from the busy heap and immediately reassigning it to the current task.
This simulation approach mirrors the real-world scenario: at each moment, we check what resources have become available, then make the best assignment decision based on current availability. The heaps give us efficient access to both the best available server and the next server to become available.
Learn more about Heap (Priority Queue) patterns.
Solution Approach
Let's implement the solution using two min-heaps as discussed:
1. Initialize the Heaps:
- Create an
idle
heap containing all servers as tuples(weight, index)
- Create an empty
busy
heap that will store tuples(free_time, weight, index)
- Initialize an empty result array
ans
idle = [(x, i) for i, x in enumerate(servers)]
heapify(idle)
busy = []
ans = []
2. Process Each Task:
For each task j
with duration t
:
Step 2a: Free Up Servers
Check if any servers in the busy
heap have become free by time j
:
while busy and busy[0][0] <= j: _, s, i = heappop(busy) heappush(idle, (s, i))
This moves all servers that are free at or before the current time back to the idle
heap.
Step 2b: Assign Task to Server We have two cases:
Case 1: There are idle servers available
if idle: s, i = heappop(idle) heappush(busy, (j + t, s, i))
- Extract the server with smallest weight (and smallest index as tiebreaker)
- Add it to the
busy
heap with its free time asj + t
(current time + task duration)
Case 2: No idle servers (all servers are busy)
else: w, s, i = heappop(busy) heappush(busy, (w + t, s, i))
- Take the server that will be free earliest from the
busy
heap - Reassign it immediately with new free time as
w + t
(its original free time + task duration) - This effectively "fast-forwards" to when this server becomes available
Step 2c: Record the Assignment
ans.append(i)
Add the server index to our answer array.
3. Return the Result:
After processing all tasks, return the ans
array containing the server index for each task.
Time Complexity: O((n + m) log n)
where:
n
is the number of serversm
is the number of tasks- Each heap operation takes
O(log n)
time - We perform heap operations for each task and potentially for each server
Space Complexity: O(n)
for storing the two heaps containing server information.
The elegance of this solution lies in using the heap's natural ordering to handle the priority rules automatically, and the two-heap pattern to efficiently track server states (idle vs busy).
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 servers [5, 1, 4]
and tasks [2, 1, 1, 3]
.
Initial Setup:
idle
heap:[(1, 1), (4, 2), (5, 0)]
(sorted by weight, then index)busy
heap:[]
(empty)ans
:[]
Task 0 (duration=2, arrives at second 0):
- No servers to free (busy heap is empty)
- Pop from idle: server 1 (weight=1)
- Server 1 will be free at time 0+2=2
- Push to busy:
(2, 1, 1)
ans = [1]
Task 1 (duration=1, arrives at second 1):
- Check busy heap: server 1 free at time 2, current time is 1 → no servers to free
- Pop from idle: server 2 (weight=4)
- Server 2 will be free at time 1+1=2
- Push to busy:
(2, 4, 2)
ans = [1, 2]
Task 2 (duration=1, arrives at second 2):
- Check busy heap: both servers free at time 2, current time is 2 → free both!
- Pop
(2, 1, 1)
→ push(1, 1)
to idle - Pop
(2, 4, 2)
→ push(4, 2)
to idle
- Pop
- idle heap now:
[(1, 1), (4, 2), (5, 0)]
- Pop from idle: server 1 (weight=1)
- Server 1 will be free at time 2+1=3
- Push to busy:
(3, 1, 1)
ans = [1, 2, 1]
Task 3 (duration=3, arrives at second 3):
- Check busy heap: server 1 free at time 3, current time is 3 → free it!
- Pop
(3, 1, 1)
→ push(1, 1)
to idle
- Pop
- idle heap now:
[(1, 1), (4, 2), (5, 0)]
- Pop from idle: server 1 (weight=1)
- Server 1 will be free at time 3+3=6
- Push to busy:
(6, 1, 1)
ans = [1, 2, 1, 1]
Final Result: [1, 2, 1, 1]
The key observations:
- When multiple servers become free simultaneously (at second 2), they all get moved to idle before assignment
- The heap automatically maintains our priority rules: smallest weight first, then smallest index
- Server 1 (weight=1) gets picked most often due to its low weight
- Server 0 (weight=5) never gets used because lighter servers are always available
Solution Implementation
1class Solution:
2 def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
3 # Create min heap of idle servers: (weight, server_index)
4 # Servers with lower weight have higher priority
5 idle_servers = [(weight, index) for index, weight in enumerate(servers)]
6 heapify(idle_servers)
7
8 # Min heap of busy servers: (available_time, weight, server_index)
9 # Sorted by when they become available
10 busy_servers = []
11
12 # Result array to store which server handles each task
13 result = []
14
15 # Process each task at its arrival time
16 for task_index, task_duration in enumerate(tasks):
17 # Free up servers that have completed their tasks by current time
18 while busy_servers and busy_servers[0][0] <= task_index:
19 available_time, server_weight, server_index = heappop(busy_servers)
20 heappush(idle_servers, (server_weight, server_index))
21
22 # Assign task to an available server if one exists
23 if idle_servers:
24 server_weight, server_index = heappop(idle_servers)
25 # Server will be busy until: current_time + task_duration
26 heappush(busy_servers, (task_index + task_duration, server_weight, server_index))
27 else:
28 # No idle servers - wait for the earliest busy server to finish
29 available_time, server_weight, server_index = heappop(busy_servers)
30 # Server will be busy until: its_available_time + task_duration
31 heappush(busy_servers, (available_time + task_duration, server_weight, server_index))
32
33 # Record which server is handling this task
34 result.append(server_index)
35
36 return result
37
1class Solution {
2 public int[] assignTasks(int[] servers, int[] tasks) {
3 int numServers = servers.length;
4
5 // Priority queue for idle servers
6 // Sorted by: 1) server weight (ascending), 2) server index (ascending)
7 PriorityQueue<int[]> idleServers = new PriorityQueue<>((a, b) -> {
8 if (a[0] != b[0]) {
9 return a[0] - b[0]; // Compare by weight
10 }
11 return a[1] - b[1]; // Compare by index if weights are equal
12 });
13
14 // Priority queue for busy servers
15 // Sorted by: 1) free time (ascending), 2) server weight (ascending), 3) server index (ascending)
16 PriorityQueue<int[]> busyServers = new PriorityQueue<>((a, b) -> {
17 if (a[0] != b[0]) {
18 return a[0] - b[0]; // Compare by free time
19 }
20 if (a[1] != b[1]) {
21 return a[1] - b[1]; // Compare by weight if free times are equal
22 }
23 return a[2] - b[2]; // Compare by index if both free time and weight are equal
24 });
25
26 // Initialize idle servers queue with all servers
27 // Each entry: [weight, index]
28 for (int i = 0; i < numServers; i++) {
29 idleServers.offer(new int[] {servers[i], i});
30 }
31
32 int numTasks = tasks.length;
33 int[] result = new int[numTasks];
34
35 // Process each task
36 for (int taskIndex = 0; taskIndex < numTasks; taskIndex++) {
37 int taskDuration = tasks[taskIndex];
38
39 // Move servers that have become free at current time back to idle queue
40 while (!busyServers.isEmpty() && busyServers.peek()[0] <= taskIndex) {
41 int[] freedServer = busyServers.poll();
42 // Add back to idle queue: [weight, index]
43 idleServers.offer(new int[] {freedServer[1], freedServer[2]});
44 }
45
46 // Assign task to a server
47 if (!idleServers.isEmpty()) {
48 // Use an idle server
49 int serverIndex = idleServers.poll()[1];
50 result[taskIndex] = serverIndex;
51 // Add to busy queue: [free time, weight, index]
52 busyServers.offer(new int[] {taskIndex + taskDuration, servers[serverIndex], serverIndex});
53 } else {
54 // All servers are busy, wait for the earliest one to become free
55 int[] earliestFreeServer = busyServers.poll();
56 int serverIndex = earliestFreeServer[2];
57 result[taskIndex] = serverIndex;
58 // Reschedule server: [new free time, weight, index]
59 busyServers.offer(new int[] {earliestFreeServer[0] + taskDuration, earliestFreeServer[1], serverIndex});
60 }
61 }
62
63 return result;
64 }
65}
66
1class Solution {
2public:
3 vector<int> assignTasks(vector<int>& servers, vector<int>& tasks) {
4 // Define type aliases for clarity
5 using ServerInfo = pair<int, int>; // {weight, index}
6 using BusyServer = array<int, 3>; // {free_time, weight, index}
7
8 // Min-heap for idle servers (sorted by weight, then by index)
9 priority_queue<ServerInfo, vector<ServerInfo>, greater<ServerInfo>> idleServers;
10
11 // Min-heap for busy servers (sorted by free time, then weight, then index)
12 priority_queue<BusyServer, vector<BusyServer>, greater<BusyServer>> busyServers;
13
14 // Initialize all servers as idle
15 for (int i = 0; i < servers.size(); ++i) {
16 idleServers.push({servers[i], i});
17 }
18
19 int numTasks = tasks.size();
20 vector<int> result(numTasks);
21
22 // Process each task in order
23 for (int taskIndex = 0; taskIndex < numTasks; ++taskIndex) {
24 int taskDuration = tasks[taskIndex];
25
26 // Free up servers that have completed their tasks by current time
27 while (!busyServers.empty() && busyServers.top()[0] <= taskIndex) {
28 auto [freeTime, serverWeight, serverIndex] = busyServers.top();
29 busyServers.pop();
30 idleServers.push({serverWeight, serverIndex});
31 }
32
33 // Assign task to an available server
34 if (!idleServers.empty()) {
35 // Use the idle server with smallest weight (and smallest index if tied)
36 auto [serverWeight, serverIndex] = idleServers.top();
37 idleServers.pop();
38 result[taskIndex] = serverIndex;
39
40 // Mark server as busy until taskIndex + taskDuration
41 busyServers.push({taskIndex + taskDuration, serverWeight, serverIndex});
42 } else {
43 // All servers are busy, wait for the earliest one to become free
44 auto [freeTime, serverWeight, serverIndex] = busyServers.top();
45 busyServers.pop();
46 result[taskIndex] = serverIndex;
47
48 // Server will be busy for additional taskDuration time from its free time
49 busyServers.push({freeTime + taskDuration, serverWeight, serverIndex});
50 }
51 }
52
53 return result;
54 }
55};
56
1// Import or assume PriorityQueue is available
2declare class PriorityQueue<T> {
3 constructor(options: { compare: (a: T, b: T) => number });
4 enqueue(item: T): void;
5 dequeue(): T | undefined;
6 front(): T | undefined;
7 size(): number;
8}
9
10/**
11 * Assigns tasks to servers based on their weight and availability
12 * @param servers - Array of server weights
13 * @param tasks - Array of task durations
14 * @returns Array of server indices that handle each task
15 */
16function assignTasks(servers: number[], tasks: number[]): number[] {
17 // Priority queue for idle servers: [weight, index]
18 // Sorted by weight first, then by index if weights are equal
19 const idleServers = new PriorityQueue<[number, number]>({
20 compare: (a, b) => {
21 if (a[0] === b[0]) {
22 return a[1] - b[1]; // Compare by index if weights are equal
23 }
24 return a[0] - b[0]; // Compare by weight
25 }
26 });
27
28 // Priority queue for busy servers: [availableTime, weight, index]
29 // Sorted by available time, then weight, then index
30 const busyServers = new PriorityQueue<[number, number, number]>({
31 compare: (a, b) => {
32 if (a[0] === b[0]) { // Same available time
33 if (a[1] === b[1]) { // Same weight
34 return a[2] - b[2]; // Compare by index
35 }
36 return a[1] - b[1]; // Compare by weight
37 }
38 return a[0] - b[0]; // Compare by available time
39 }
40 });
41
42 // Initialize all servers as idle
43 for (let serverIndex = 0; serverIndex < servers.length; serverIndex++) {
44 idleServers.enqueue([servers[serverIndex], serverIndex]);
45 }
46
47 // Result array to store which server handles each task
48 const result: number[] = [];
49
50 // Process each task in order
51 for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
52 const taskDuration = tasks[taskIndex];
53
54 // Move servers that have become idle back to idle queue
55 while (busyServers.size() > 0 && busyServers.front()![0] <= taskIndex) {
56 const [availableTime, serverWeight, serverIndex] = busyServers.dequeue()!;
57 idleServers.enqueue([serverWeight, serverIndex]);
58 }
59
60 // Assign task to an available server
61 if (idleServers.size() > 0) {
62 // Use an idle server
63 const [serverWeight, serverIndex] = idleServers.dequeue()!;
64 busyServers.enqueue([taskIndex + taskDuration, serverWeight, serverIndex]);
65 result.push(serverIndex);
66 } else {
67 // All servers are busy, wait for the next available server
68 const [availableTime, serverWeight, serverIndex] = busyServers.dequeue()!;
69 busyServers.enqueue([availableTime + taskDuration, serverWeight, serverIndex]);
70 result.push(serverIndex);
71 }
72 }
73
74 return result;
75}
76
Time and Space Complexity
Time Complexity: O(m * log n + n * log n)
where n
is the number of servers and m
is the number of tasks.
- Initial heap construction from the servers list takes
O(n)
time usingheapify
. - For each of the
m
tasks:- The while loop that moves servers from busy to idle heap: In the worst case, all servers could be moved, but amortized across all iterations, each server moves from busy to idle at most once per task assignment. Each heap operation (pop and push) takes
O(log n)
. - Popping from idle heap:
O(log n)
- Pushing to busy heap:
O(log n)
- Or if idle is empty, popping and pushing to busy heap:
O(log n)
- The while loop that moves servers from busy to idle heap: In the worst case, all servers could be moved, but amortized across all iterations, each server moves from busy to idle at most once per task assignment. Each heap operation (pop and push) takes
- Overall, we have
m
iterations, each withO(log n)
operations, plus potential server transfers that sum toO(m * log n)
amortized. - Total:
O(n + m * log n)
which simplifies toO(m * log n + n * log n)
since typicallym ≥ n
in practical scenarios.
Space Complexity: O(n)
- The
idle
heap stores at mostn
servers:O(n)
- The
busy
heap stores at mostn
servers:O(n)
- The
ans
list storesm
elements:O(m)
- Since the output array doesn't count toward space complexity in the analysis (as it's required output), the auxiliary space is
O(n)
for the two heaps.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Time Tracking When All Servers Are Busy
The Problem:
A critical mistake occurs when assigning tasks to busy servers. Many implementations incorrectly calculate the new availability time as current_time + task_duration
instead of server_available_time + task_duration
.
Incorrect Implementation:
if not idle_servers: available_time, server_weight, server_index = heappop(busy_servers) # WRONG: Using task_index instead of available_time heappush(busy_servers, (task_index + task_duration, server_weight, server_index))
Why This Fails:
When a task arrives at second 5 but the earliest available server won't be free until second 10, the task must wait. The server should become available at second 10 + task_duration
, not 5 + task_duration
.
Correct Implementation:
if not idle_servers: available_time, server_weight, server_index = heappop(busy_servers) # CORRECT: Using the server's available_time heappush(busy_servers, (available_time + task_duration, server_weight, server_index))
Pitfall 2: Not Freeing All Available Servers Simultaneously
The Problem:
Using if
instead of while
when checking for free servers, causing only one server to be freed per task even when multiple servers become available at the same time.
Incorrect Implementation:
# WRONG: Only frees one server if busy_servers and busy_servers[0][0] <= task_index: available_time, server_weight, server_index = heappop(busy_servers) heappush(idle_servers, (server_weight, server_index))
Why This Fails: If servers 0 and 1 both become free at second 3, and task 3 arrives at second 3, we need both servers in the idle pool to correctly choose the one with smallest weight/index.
Correct Implementation:
# CORRECT: Frees all servers that are available while busy_servers and busy_servers[0][0] <= task_index: available_time, server_weight, server_index = heappop(busy_servers) heappush(idle_servers, (server_weight, server_index))
Pitfall 3: Misunderstanding Heap Ordering for Tie-Breaking
The Problem: Forgetting that Python's heaps naturally handle tuple comparisons lexicographically, which automatically provides the correct tie-breaking behavior.
Over-complicated Implementation:
# Unnecessary: Creating custom comparison logic
idle_servers = []
for i, weight in enumerate(servers):
heappush(idle_servers, (weight * 100000 + i)) # Trying to encode both values
Why This is Problematic: This approach is error-prone, can cause integer overflow with large values, and makes the code harder to understand.
Clean Implementation:
# Python automatically compares tuples element by element
idle_servers = [(weight, index) for index, weight in enumerate(servers)]
heapify(idle_servers)
# (3, 0) < (3, 1) < (4, 0) - correct ordering automatically
Complete Corrected Solution:
from heapq import heappush, heappop, heapify
from typing import List
class Solution:
def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
idle_servers = [(weight, index) for index, weight in enumerate(servers)]
heapify(idle_servers)
busy_servers = []
result = []
for task_index, task_duration in enumerate(tasks):
# Free ALL servers that are available by current time
while busy_servers and busy_servers[0][0] <= task_index:
available_time, server_weight, server_index = heappop(busy_servers)
heappush(idle_servers, (server_weight, server_index))
if idle_servers:
server_weight, server_index = heappop(idle_servers)
heappush(busy_servers, (task_index + task_duration, server_weight, server_index))
else:
# Use server's available_time, not current task_index
available_time, server_weight, server_index = heappop(busy_servers)
heappush(busy_servers, (available_time + task_duration, server_weight, server_index))
result.append(server_index)
return result
In a binary min heap, the maximum element can be found in:
Recommended Readings
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!