1882. Process Tasks Using Servers
Problem Description
In this problem, we're given two arrays, servers
and tasks
. The servers
array contains the weights of each server, which represent their capacity to handle tasks. The tasks
array contains the times needed to process each task in seconds. Tasks arrive at each second starting from the 0th second and are queued to be processed by the servers.
The goal is to assign each task to a server as efficiently as possible, obeying the following rules:
- If a server is available, assign the task at the front of the queue to the server with the smallest weight. If there's a tie in weight, assign it to the server with the lower index.
- If no servers are available, the task waits in the queue until a server is free.
- When multiple servers become available at the same time, tasks are assigned in the order they entered the queue, again preferring servers with smaller weight or index.
A server is considered free again once it has finished processing its assigned task, which takes a number of seconds equal to the task's processing time. The output should be an array indicating the index of the server each task is assigned to.
Intuition
The solution requires managing two types of priority queues: one for idle servers (idle
) and one for busy servers (busy
). Priority queues are data structures that allow us to maintain a set of elements ordered by some priority, which makes it ideal for this task.
For the idle
queue, the priority is based first on server weight, then on index. This ensures that we always have quick access to the smallest server that is free. The priority for the busy
queue is the time when the server will become free—this means we know the order in which servers will finish their tasks.
Here's a step-by-step approach:
- Initialize two heaps: one for all the idle servers (
idle
), filled with the servers' weight and index, and anotherbusy
heap for servers that are currently processing tasks. Thebusy
heap will be initially empty. - Loop through each task (by its start time), doing the following:
- Pop servers that have finished their tasks from the
busy
heap to theidle
heap. - Check if there are servers available in the
idle
heap:- If yes, pop the server with the smallest weight (and smallest index in case of a tie) from the
idle
heap and assign the current task to it. Then push it onto thebusy
heap with the time it will become free after processing the current task. - If not, take the server that will be free next from the
busy
heap, update the time it will be free by adding the current task's time, and push it back onto thebusy
heap.
- If yes, pop the server with the smallest weight (and smallest index in case of a tie) from the
- Pop servers that have finished their tasks from the
- Append the index of the server that was assigned the task to the result array, which will later be returned.
This way, we effectively distribute tasks among servers based on their availability and weight, ensuring an efficient task scheduling.
Learn more about Heap (Priority Queue) patterns.
Solution Approach
The implementation of the solution involves using Python's heapq
module, which provides an implementation of the priority queue algorithm, also known as the heap queue or binary heap.
Here are the steps in detail:
-
Create two heaps:
idle
for the free servers andbusy
for the servers currently processing tasks. Theidle
heap will be a min-heap based on(weight, index)
, while thebusy
heap will be a min-heap based on(free_time, weight, index)
. -
For each server, push a tuple
(weight, index)
onto theidle
heap. This prepares our system to know which servers are free to be assigned tasks. -
Iterate through each task using its index
start
as the current second and the corresponding valuecost
as the time it takes to complete the task. -
Before assigning a task, free up any servers that have completed their tasks by the current second (
start
). We repeatedly pop from thebusy
heap until we find a server that is still busy. The popped servers (which are now free) are pushed onto theidle
heap.while busy and busy[0][0] <= start: _, s, i = heappop(busy) heappush(idle, (s, i))
-
Assign a task to a server:
-
If the
idle
heap is not empty, pop the server with the smallest weight (and lowest index in case of a tie). Push a tuple(start + cost, weight, index)
onto thebusy
heap, indicating that this server will be busy untilstart + cost
.if idle: s, i = heappop(idle) heappush(busy, (start + cost, s, i))
-
If the
idle
heap is empty, pop the server that will become free the earliest from thebusy
heap, update its free time tot + cost
, and push it back onto thebusy
heap.else: t, s, i = heappop(busy) heappush(busy, (t + cost, s, i))
-
-
Append the index of the assigned server to the result list
res
. -
After completing the iterations for all tasks, return the
res
list, which contains the index of the server that each task was assigned to.
This solution efficiently distributes tasks among servers, ensuring that the server with the smallest weight is preferred, and if all servers are busy, the task will wait in a queue until the next server becomes available, all the while following the rules described in the problem statement.
class Solution:
def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
idle, busy = [], []
for i, weight in enumerate(servers):
heappush(idle, (weight, i))
res = []
for start, cost in enumerate(tasks):
while busy and busy[0][0] <= start:
_, s, i = heappop(busy)
heappush(idle, (s, i))
if idle:
s, i = heappop(idle)
heappush(busy, (start + cost, s, i))
else:
t, s, i = heappop(busy)
heappush(busy, (t + cost, s, i))
res.append(i)
return res
The use of heaps simplifies the retrieval of the minimum weight server and the earliest server to become free. This is an important aspect of the solution, keeping the time complexity under control as we process each task.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Suppose we have 2 servers with weights [3, 3]
and 4 tasks with times [1, 2, 3, 2]
.
We initialize our idle
heap with servers as (weight, index)
making idle = [(3, 0), (3, 1)]
. The busy
heap is initially empty.
Now we process each task one by one:
-
At time
0
, task0
with time1
second arrives. Server(3, 0)
is chosen because it has the smallest index. It will be busy until time0 + 1
. Thebusy
heap becomes[(1, 3, 0)]
, andidle
becomes[(3, 1)]
. -
At time
1
, task1
with time2
seconds arrives. First, we check whether any server has become idle. Server(3, 0)
becomes idle at time1
and is moved back toidle
heap.idle = [(3, 0), (3, 1)]
. Then, we take server(3, 0)
again for this task and push it onto thebusy
heap with(1 + 2, 3, 0)
.Idle
heap is now[(3, 1)]
, andbusy = [(3, 3, 0)]
. -
At time
2
, task2
with time3
seconds arrives. There is a server available inidle
heap. Server(3, 1)
is chosen. It will be busy until2 + 3
.idle
heap is now empty, andbusy
heap is updated to[(3, 3, 0), (5, 3, 1)]
. -
At time
3
, task3
with time2
seconds arrives. There is no server available inidle
heap. We look at thebusy
heap. The server that will be free next is(3, 3, 0)
, but it is not yet free until time3
. So we update its free time to3 + 2 = 5
and push it back onto thebusy
heap which now looks like[(5, 3, 0), (5, 3, 1)]
sorted by the earliest free time, weight, and then index.
After all tasks are processed, we have assigned the tasks as follows: [0, 0, 1, 0]
.
The servers 0
, 0
, and 1
handle tasks 0
, 1
, and 2
respectively, and once server 0
is free again at time 3
, it also takes on task 3
.
Solution Implementation
1from heapq import heappush, heappop
2
3class Solution:
4 def assign_tasks(self, servers: List[int], tasks: List[int]) -> List[int]:
5 # Min-heap for idle servers with tuple structure: (weight, index)
6 idle_servers_heap = []
7
8 # Min-heap for busy servers with tuple structure: (time it gets free, weight, index)
9 busy_servers_heap = []
10
11 # Fill the idle heap with initial server states
12 for index, weight in enumerate(servers):
13 heappush(idle_servers_heap, (weight, index))
14
15 # List to store the server index that takes each task
16 task_assignments = []
17
18 # Iterate through tasks and assign servers
19 for start_time, task_duration in enumerate(tasks):
20 # Free up servers that have completed their tasks by current time
21 while busy_servers_heap and busy_servers_heap[0][0] <= start_time:
22 _, server_weight, server_index = heappop(busy_servers_heap)
23 heappush(idle_servers_heap, (server_weight, server_index))
24
25 # Assign a server to the task
26 if idle_servers_heap:
27 # Pop from idle heap as server becomes busy
28 server_weight, server_index = heappop(idle_servers_heap)
29 # Server will be free at current time + task duration
30 heappush(busy_servers_heap, (start_time + task_duration, server_weight, server_index))
31 else:
32 # No idle server available, wait for the next server to become idle
33 free_time, server_weight, server_index = heappop(busy_servers_heap)
34 # Server will be free again after it completes this task
35 heappush(busy_servers_heap, (free_time + task_duration, server_weight, server_index))
36
37 # Record the assignment
38 task_assignments.append(server_index)
39
40 return task_assignments
41
1class Solution {
2 public int[] assignTasks(int[] servers, int[] tasks) {
3 int numTasks = tasks.length, numServers = servers.length;
4 // Create a priority queue to store idle servers, prioritizing by weight then index.
5 PriorityQueue<int[]> idleServers = new PriorityQueue<>(
6 (server1, server2) ->
7 server1[0] == server2[0] ? server1[1] - server2[1] : server1[0] - server2[0]
8 );
9
10 // Create a priority queue to store busy servers, prioritizing by finish time,
11 // then weight, then index.
12 PriorityQueue<int[]> busyServers = new PriorityQueue<>(
13 (server1, server2) -> {
14 if (server1[0] == server2[0]) {
15 return server1[1] == server2[1] ? server1[2] - server2[2] : server1[1] - server2[1];
16 }
17 return server1[0] - server2[0];
18 }
19 );
20
21 // Initialize the idle servers priority queue with all servers.
22 for (int i = 0; i < numServers; ++i) {
23 idleServers.offer(new int[]{servers[i], i}); // [weight, index]
24 }
25
26 // Initialize an array to hold the server assignment results for each task.
27 int[] assignedServers = new int[numTasks];
28 int assignedTasksCount = 0;
29
30 // Iterate over each second to assign tasks to servers.
31 for (int currentTime = 0; currentTime < numTasks; ++currentTime) {
32 int taskDuration = tasks[currentTime];
33
34 // Release all servers that have completed their tasks by this second.
35 while (!busyServers.isEmpty() && busyServers.peek()[0] <= currentTime) {
36 int[] server = busyServers.poll();
37 idleServers.offer(new int[] {server[1], server[2]}); // [weight, index]
38 }
39
40 // Assign a server to the current task.
41 if (!idleServers.isEmpty()) {
42 // If there are available servers, poll the best one.
43 int[] server = idleServers.poll();
44 assignedServers[assignedTasksCount++] = server[1];
45 busyServers.offer(new int[] {currentTime + taskDuration, server[0], server[1]});
46 } else {
47 // If no servers are idle, poll the server that will become available the soonest.
48 int[] server = busyServers.poll();
49 assignedServers[assignedTasksCount++] = server[2];
50 busyServers.offer(new int[] {server[0] + taskDuration, server[1], server[2]});
51 }
52 }
53 return assignedServers;
54 }
55}
56
1#include <vector>
2#include <queue>
3
4class Solution {
5public:
6 std::vector<int> assignTasks(std::vector<int>& servers, std::vector<int>& tasks) {
7 int numTasks = tasks.size(), numServers = servers.size();
8
9 // Lambda to compare servers based on weight and then index.
10 auto cmpServer = [](const std::vector<int>& server1, const std::vector<int>& server2) {
11 if (server1[0] == server2[0]) return server1[1] > server2[1];
12 return server1[0] > server2[0];
13 };
14
15 // Lambda to compare busy servers based on finish time, then weight, then index.
16 auto cmpBusyServer = [](const std::vector<int>& server1, const std::vector<int>& server2) {
17 if (server1[0] == server2[0]) {
18 if (server1[1] == server2[1]) return server1[2] > server2[2];
19 return server1[1] > server2[1];
20 }
21 return server1[0] > server2[0];
22 };
23
24 // Priority queue to store idle servers, sorted by weight then index.
25 std::priority_queue<std::vector<int>, std::vector<std::vector<int>>, decltype(cmpServer)> idleServers(cmpServer);
26
27 // Priority queue to store busy servers, sorted by finish time, weight, then index.
28 std::priority_queue<std::vector<int>, std::vector<std::vector<int>>, decltype(cmpBusyServer)> busyServers(cmpBusyServer);
29
30 // Initialize the idle servers priority queue with all servers.
31 for (int i = 0; i < numServers; ++i) {
32 idleServers.push({servers[i], i}); // [weight, index]
33 }
34
35 // Initialize a vector to hold the server assignment results for each task.
36 std::vector<int> assignedServers(numTasks, 0);
37 int assignedTasksCount = 0;
38
39 // Iterate over each second to assign tasks to servers.
40 for (int currentTime = 0; currentTime < numTasks; ++currentTime) {
41 int taskDuration = tasks[currentTime];
42
43 // Free up servers that have completed their tasks by this time.
44 while (!busyServers.empty() && busyServers.top()[0] <= currentTime) {
45 std::vector<int> server = busyServers.top();
46 busyServers.pop();
47 idleServers.push({server[1], server[2]}); // [weight, index]
48 }
49
50 // Assign a server to the current task.
51 if (!idleServers.empty()) {
52 // If there are available servers, get the top one.
53 std::vector<int> server = idleServers.top();
54 idleServers.pop();
55 assignedServers[assignedTasksCount++] = server[1];
56 busyServers.push({currentTime + taskDuration, server[0], server[1]});
57 } else {
58 // If no servers are idle, take the server that will be available soonest.
59 std::vector<int> server = busyServers.top();
60 busyServers.pop();
61 assignedServers[assignedTasksCount++] = server[2];
62 busyServers.push({server[0] + taskDuration, server[1], server[2]});
63 }
64 }
65
66 return assignedServers;
67 }
68};
69
1// Define a comparator for idle servers prioritizing by weight then index
2const idleServerComparator = (server1: [number, number], server2: [number, number]) =>
3 server1[0] === server2[0] ? server1[1] - server2[1] : server1[0] - server2[0];
4
5// Define a comparator for busy servers prioritizing by finish time, then weight, then index
6const busyServerComparator = (server1: [number, number, number], server2: [number, number, number]) =>
7 server1[0] === server2[0] ? (server1[1] === server2[1] ? server1[2] - server2[2] : server1[1] - server2[1]) : server1[0] - server2[0];
8
9// Define priority queues using the comparators
10const idleServers: [number, number][] = [];
11const busyServers: [number, number, number][] = [];
12
13// Function to insert an element into a priority queue using a comparator
14const insert = <T>(pq: T[], element: T, comparator: (a: T, b: T) => number): void => {
15 pq.push(element);
16 pq.sort(comparator);
17};
18
19// Function to extract the first element from a priority queue
20const extract = <T>(pq: T[]): T | undefined => {
21 return pq.shift();
22};
23
24/**
25 * Assigns tasks to servers based on their weights and availability.
26 * @param servers Array of server weights
27 * @param tasks Array of task durations
28 * @returns Array of server indices indicating server assignment for each task
29 */
30const assignTasks = (servers: number[], tasks: number[]): number[] => {
31 const numTasks = tasks.length;
32 const numServers = servers.length;
33
34 // Initialize the idle servers priority queue with all servers
35 servers.forEach((weight, index) => insert(idleServers, [weight, index], idleServerComparator));
36
37 // Array to hold the server assignments for each task
38 const assignedServers: number[] = [];
39
40 // Iterate over each second to assign tasks to servers
41 for (let currentTime = 0; currentTime < numTasks; ++currentTime) {
42 const taskDuration = tasks[currentTime];
43
44 // Release all servers that have completed their tasks by this second
45 while (busyServers.length > 0 && busyServers[0][0] <= currentTime) {
46 const [finishTime, weight, index] = extract(busyServers)!;
47 insert(idleServers, [weight, index], idleServerComparator);
48 }
49
50 // If there are available servers, assign the best one
51 if (idleServers.length > 0) {
52 const [weight, index] = extract(idleServers)!;
53 assignedServers.push(index);
54 insert(busyServers, [currentTime + taskDuration, weight, index], busyServerComparator);
55 } else {
56 // If no servers are idle, poll the server that will become available the soonest
57 const [finishTime, weight, index] = extract(busyServers)!;
58 assignedServers.push(index);
59 insert(busyServers, [finishTime + taskDuration, weight, index], busyServerComparator);
60 }
61 }
62 return assignedServers;
63};
64
Time and Space Complexity
Time Complexity
The time complexity of the given code mainly depends on the loops and the heap operations.
-
The initial loop to push each server into the idle heap runs exactly
n
times wheren
is the number of servers. This is a constant time operation for each server, henceO(n)
. -
The main loop iterates over all tasks, which results in
O(m)
iterations wherem
is the number of tasks. -
Inside the main loop, there's a
while
loop that pops from the busy heap. In the worst case, this can run for as many tasks there are that were previously assigned, however, each task can only cause a busy-to-idle pop once, so across all iterations, this step isO(m * log(n))
as each pop operation isO(log(n))
and there are at mostm
such operations. -
The
if
condition and theelse
block involve a heap push and pop operation respectively. Each operation takesO(log(n))
time, and since they are called for each task, the time complexity for the push and pop is alsoO(m * log(n))
.
This gives us a total time complexity of O(n + m * log(n))
where n
is the number of servers and m
is the number of tasks.
Space Complexity
The space complexity is determined by the space required for the idle
and the busy
heaps, as well as the space for the res
array.
- The
idle
heap can contain at mostn
server entries. - The
busy
heap can contain at mostm
task entries at any time, which would be the case when all tasks have started but none have completed.
Therefore, the space complexity is O(n + m)
for the heaps plus O(m)
for the result list, which simplifies to O(n + m)
overall.
Learn more about how to find time and space complexity quickly using problem constraints.
How many times is a tree node visited in a depth first search?
Recommended Readings
https algomonster s3 us east 2 amazonaws com 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
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!