Facebook Pixel

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:

  1. Task Arrival: Tasks arrive sequentially - the j-th task arrives at second j (starting from second 0 for the 0-th task).

  2. 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
  3. 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).

  4. Server Availability: When a server starts processing task j at time t, it becomes free again at time t + tasks[j].

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

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

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

  2. 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 as j + 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 servers
  • m 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 Evaluator

Example 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
  • 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
  • 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:

  1. When multiple servers become free simultaneously (at second 2), they all get moved to idle before assignment
  2. The heap automatically maintains our priority rules: smallest weight first, then smallest index
  3. Server 1 (weight=1) gets picked most often due to its low weight
  4. 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 using heapify.
  • 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)
  • Overall, we have m iterations, each with O(log n) operations, plus potential server transfers that sum to O(m * log n) amortized.
  • Total: O(n + m * log n) which simplifies to O(m * log n + n * log n) since typically m ≥ n in practical scenarios.

Space Complexity: O(n)

  • The idle heap stores at most n servers: O(n)
  • The busy heap stores at most n servers: O(n)
  • The ans list stores m 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More