1882. Process Tasks Using Servers - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Heap (Priority Queue) - Two min-heaps are used to efficiently track available servers (by weight/index) and busy servers (by finish time)
  • Simulation - Understanding how to model time-based task assignment and server availability
  • Custom Comparators - Implementing multi-key ordering (weight, then index) for priority queue elements

1. Brute Force (Simulation)

Intuition

We simulate the process step by step. For each task, we advance time to when the task becomes available, mark all servers that have finished as available, then pick the best available server (lowest weight, then lowest index). If no server is free, we wait until the earliest one finishes. This direct simulation is easy to understand but slow due to repeated linear scans.

Algorithm

  1. Initialize arrays to track each server's availability status and finish time.
  2. For each task at index t:
    • Set time to at least t (the task's arrival time).
    • Mark servers as available if their finish time is at or before current time.
    • If no servers are available, advance time to the earliest finish time and update availability.
    • Find the available server with the smallest weight (and smallest index for ties).
    • Assign the task to that server, mark it unavailable, and set its finish time.
  3. Return the list of assigned server indices.
class Solution:
    def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
        n, m = len(servers), len(tasks)
        available = [True] * n
        finishTime = [0] * n
        res = []
        time = 0

        for t in range(m):
            time = max(time, t)

            for i in range(n):
                if finishTime[i] <= time:
                    available[i] = True

            if not any(available):
                time = min(finishTime)
                for i in range(n):
                    if finishTime[i] <= time:
                        available[i] = True

            minIdx = -1
            for i in range(n):
                if (available[i] and
                    (minIdx == -1 or servers[i] < servers[minIdx] or
                    (servers[i] == servers[minIdx] and i < minIdx))
                ):
                    minIdx = i

            res.append(minIdx)
            available[minIdx] = False
            finishTime[minIdx] = time + tasks[t]

        return res

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(m)O(m) space for the output array.

Where mm is the number of tasks and nn is the number of servers.


2. Two Min-Heaps - I

Intuition

Using two heaps makes server selection efficient. One heap holds available servers ordered by (weight, index), and another holds unavailable servers ordered by their finish time. When processing a task, we move servers from unavailable to available if their time has passed, then pop the best server from the available heap.

Algorithm

  1. Create two heaps:
    • available: min-heap ordered by (weight, index) for free servers.
    • unavailable: min-heap ordered by finish time for busy servers.
  2. Add all servers to available.
  3. For each task at index i:
    • Set time to at least i.
    • If no server is available, advance time to the earliest finish time in unavailable.
    • Move all servers from unavailable whose finish time is at or before time to available.
    • Pop the best server from available, assign the task, and push it to unavailable with its new finish time.
  4. Return the result array.
class Solution:
    def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
        res = [0] * len(tasks)
        available = [(servers[i], i) for i in range(len(servers))]
        heapq.heapify(available)
        unavailable = []

        t = 0
        for i in range(len(tasks)):
            t = max(t, i)

            if not available:
                t = unavailable[0][0]

            while unavailable and t >= unavailable[0][0]:
                timeFree, weight, index = heapq.heappop(unavailable)
                heapq.heappush(available, (weight, index))

            weight, index = heapq.heappop(available)
            res[i] = index
            heapq.heappush(unavailable, (t + tasks[i], weight, index))

        return res

Time & Space Complexity

  • Time complexity: O((n+m)logn)O((n + m) \log n)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(m)O(m) space for the output array.

Where mm is the number of tasks and nn is the number of servers.


3. Two Min-Heaps - II

Intuition

This is a variation of the two-heap approach with slightly different state tracking. Instead of storing finish time separately, we embed the last known free time within the available heap entries. When transferring servers between heaps, we update this time accordingly. The logic remains the same: efficiently pick the best available server using heaps.

Algorithm

  1. Create two heaps:
    • available: min-heap storing (weight, index, timeFree) for available servers.
    • unavailable: min-heap storing (timeFree, weight, index) for busy servers.
  2. Add all servers to available with initial timeFree of 0.
  3. For each task at index i:
    • Move servers from unavailable to available while their finish time is at or before i, or while available is empty (must wait for a server).
    • Pop the best server from available.
    • Assign the task and push the server to unavailable with updated finish time: max(timeFree, i) + task duration.
  4. Return the result array.
class Solution:
    def assignTasks(self, servers: List[int], tasks: List[int]) -> List[int]:
        res = []
        available = [[weight, i, 0] for i, weight in enumerate(servers)]
        unavailable = []
        heapq.heapify(available)

        for i, task in enumerate(tasks):
            while unavailable and unavailable[0][0] <= i or not available:
                timeFree, weight, index = heapq.heappop(unavailable)
                heapq.heappush(available, [weight, index, timeFree])

            weight, index, timeFree = heapq.heappop(available)
            res.append(index)
            heapq.heappush(unavailable, [max(timeFree, i) + task, weight, index])

        return res

Time & Space Complexity

  • Time complexity: O((n+m)logn)O((n + m) \log n)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(m)O(m) space for the output array.

Where mm is the number of tasks and nn is the number of servers.


Common Pitfalls

Incorrect Heap Ordering for Server Selection

When no servers are available at time t, you must wait until the earliest server becomes free. A common mistake is not correctly ordering the unavailable heap by finish time, or forgetting to move all servers that become free at that time back to the available heap. Multiple servers might become available simultaneously.

Not Advancing Time When All Servers Are Busy

If all servers are busy when a task arrives, you must jump time forward to when the next server becomes free. Forgetting this causes an infinite loop or incorrect assignments. The time should be set to unavailable.peek().finishTime, not incremented by 1.

Wrong Tie-Breaking Logic in Priority Queue

The problem requires selecting the server with the smallest weight, and among ties, the smallest index. Implementing the comparator incorrectly (e.g., comparing indices before weights) leads to wrong server assignments. Ensure the primary comparison is on weight and secondary on index.