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:

  1. 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.
  2. If no servers are available, the task waits in the queue until a server is free.
  3. 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:

  1. Initialize two heaps: one for all the idle servers (idle), filled with the servers' weight and index, and another busy heap for servers that are currently processing tasks. The busy heap will be initially empty.
  2. Loop through each task (by its start time), doing the following:
    • Pop servers that have finished their tasks from the busy heap to the idle 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 the busy 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 the busy heap.
  3. 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.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Which of the following array represent a max heap?

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:

  1. Create two heaps: idle for the free servers and busy for the servers currently processing tasks. The idle heap will be a min-heap based on (weight, index), while the busy heap will be a min-heap based on (free_time, weight, index).

  2. For each server, push a tuple (weight, index) onto the idle heap. This prepares our system to know which servers are free to be assigned tasks.

  3. Iterate through each task using its index start as the current second and the corresponding value cost as the time it takes to complete the task.

  4. Before assigning a task, free up any servers that have completed their tasks by the current second (start). We repeatedly pop from the busy heap until we find a server that is still busy. The popped servers (which are now free) are pushed onto the idle heap.

    1while busy and busy[0][0] <= start:
    2    _, s, i = heappop(busy)
    3    heappush(idle, (s, i))
  5. 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 the busy heap, indicating that this server will be busy until start + cost.

      1if idle:
      2    s, i = heappop(idle)
      3    heappush(busy, (start + cost, s, i))
    • If the idle heap is empty, pop the server that will become free the earliest from the busy heap, update its free time to t + cost, and push it back onto the busy heap.

      1else:
      2    t, s, i = heappop(busy)
      3    heappush(busy, (t + cost, s, i))
  6. Append the index of the assigned server to the result list res.

  7. 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.

1class Solution:
2    def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
3        idle, busy = [], []
4        for i, weight in enumerate(servers):
5            heappush(idle, (weight, i))
6        res = []
7        for start, cost in enumerate(tasks):
8            while busy and busy[0][0] <= start:
9                _, s, i = heappop(busy)
10                heappush(idle, (s, i))
11            if idle:
12                s, i = heappop(idle)
13                heappush(busy, (start + cost, s, i))
14            else:
15                t, s, i = heappop(busy)
16                heappush(busy, (t + cost, s, i))
17            res.append(i)
18        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.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)

Example 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, task 0 with time 1 second arrives. Server (3, 0) is chosen because it has the smallest index. It will be busy until time 0 + 1. The busy heap becomes [(1, 3, 0)], and idle becomes [(3, 1)].

  • At time 1, task 1 with time 2 seconds arrives. First, we check whether any server has become idle. Server (3, 0) becomes idle at time 1 and is moved back to idle heap. idle = [(3, 0), (3, 1)]. Then, we take server (3, 0) again for this task and push it onto the busy heap with (1 + 2, 3, 0). Idle heap is now [(3, 1)], and busy = [(3, 3, 0)].

  • At time 2, task 2 with time 3 seconds arrives. There is a server available in idle heap. Server (3, 1) is chosen. It will be busy until 2 + 3. idle heap is now empty, and busy heap is updated to [(3, 3, 0), (5, 3, 1)].

  • At time 3, task 3 with time 2 seconds arrives. There is no server available in idle heap. We look at the busy heap. The server that will be free next is (3, 3, 0), but it is not yet free until time 3. So we update its free time to 3 + 2 = 5 and push it back onto the busy 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
Not Sure What to Study? Take the 2-min Quiz:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.

Time and Space Complexity

Time Complexity

The time complexity of the given code mainly depends on the loops and the heap operations.

  1. The initial loop to push each server into the idle heap runs exactly n times where n is the number of servers. This is a constant time operation for each server, hence O(n).

  2. The main loop iterates over all tasks, which results in O(m) iterations where m is the number of tasks.

  3. 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 is O(m * log(n)) as each pop operation is O(log(n)) and there are at most m such operations.

  4. The if condition and the else block involve a heap push and pop operation respectively. Each operation takes O(log(n)) time, and since they are called for each task, the time complexity for the push and pop is also O(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.

  1. The idle heap can contain at most n server entries.
  2. The busy heap can contain at most m 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.

Fast Track Your Learning with Our Quick Skills Quiz:

In a binary min heap, the maximum element can be found in:


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫