Taro Logo

Find Servers That Handled Most Number of Requests

Hard
Amazon logo
Amazon
11 views
Topics:
ArraysGreedy AlgorithmsDynamic Programming

You have k servers numbered from 0 to k-1 that are being used to handle multiple requests simultaneously. Each server has infinite computational capacity but cannot handle more than one request at a time. The requests are assigned to servers according to a specific algorithm:

  1. The i<sup>th</sup> (0-indexed) request arrives.
  2. If all servers are busy, the request is dropped (not handled at all).
  3. If the (i % k)<sup>th</sup> server is available, assign the request to that server.
  4. Otherwise, assign the request to the next available server (wrapping around the list of servers and starting from 0 if necessary). For example, if the i<sup>th</sup> server is busy, try to assign the request to the (i+1)<sup>th</sup> server, then the (i+2)<sup>th</sup> server, and so on.

You are given a strictly increasing array arrival of positive integers, where arrival[i] represents the arrival time of the i<sup>th</sup> request, and another array load, where load[i] represents the load of the i<sup>th</sup> request (the time it takes to complete). Your goal is to find the busiest server(s). A server is considered busiest if it handled the most number of requests successfully among all the servers.

Return a list containing the IDs (0-indexed) of the busiest server(s). You may return the IDs in any order.

For example:

k = 3, arrival = [1,2,3,4,5], load = [5,2,3,3,3]

In this case the output would be [1] because server 1 handles two requests, which is more than any other server.

As another example:

k = 3, arrival = [1,2,3,4], load = [1,2,1,2]

In this case the output would be [0] because server 0 handles two requests, which is more than any other server.

Can you describe an algorithm to solve this problem efficiently?

Solution


Naive Approach

A brute force solution would involve iterating through each request and simulating the server assignment process as described in the problem statement. For each request, we'd check the availability of servers starting from (i % k) and wrapping around until an available server is found. If all servers are busy, the request is dropped. We'd maintain a count of requests handled by each server and, at the end, identify the busiest servers.

Code (Python):

def busiest_servers_naive(k, arrival, load):
    servers = [0] * k  # 0 indicates available, time indicates busy until
    counts = [0] * k
    
    for i in range(len(arrival)):
        start_server = i % k
        assigned = False
        
        for j in range(k):
            server_index = (start_server + j) % k
            if servers[server_index] <= arrival[i]:
                servers[server_index] = arrival[i] + load[i]
                counts[server_index] += 1
                assigned = True
                break
                
        if not assigned:
            pass #request dropped
            
    max_count = max(counts)
    busiest = [i for i, count in enumerate(counts) if count == max_count]
    return busiest

Time Complexity: O(n*k), where n is the number of requests (length of arrival) and k is the number of servers.

Space Complexity: O(k), for the servers and counts arrays.

Optimal Approach

To optimize, we can use a priority queue (min-heap) to keep track of available servers and their indices. We also use a sorted set to store the indices of servers that are currently processing requests and will become available in the future.

  1. Available Servers: A min-heap will store the indices of the available servers. When a server becomes available, its index is added to the heap. This allows us to quickly find the next available server after the "ideal" i % k server.
  2. Busy Servers: We use a sorted set to keep track of when servers will become free. This allows us to efficiently determine what servers have completed their tasks and are once again free for incoming requests.
  3. Server Selection: For each request, try to assign it to the server i % k. If that server is available (its index is at the top of the min-heap), assign the task to it. If not, poll the available servers until we find one >= i % k. If we reach the end, start at 0. If the available servers is empty, drop the request.

Code (Python):

import heapq
from sortedcontainers import SortedSet

def busiest_servers(k, arrival, load):
    available = list(range(k))
    heapq.heapify(available)
    busy = SortedSet()
    counts = [0] * k

    for i in range(len(arrival)):
        while busy and busy[0][0] <= arrival[i]:
            _, server_index = busy.pop(0)
            heapq.heappush(available, server_index)

        if not available:
            continue

        start_server = i % k
        
        # Find the next available server index >= start_server
        server_index = -1
        temp_available = []
        while available:
            s = heapq.heappop(available)
            if s >= start_server:
                server_index = s
                break
            else:
                temp_available.append(s)
        
        # If no available server found >= start_server, take the smallest
        if server_index == -1:
            if temp_available:
                server_index = temp_available.pop(0)
            else:
                continue #no available server
                
        for s in temp_available:
            heapq.heappush(available, s)

        counts[server_index] += 1
        busy.add((arrival[i] + load[i], server_index))

    max_count = max(counts)
    busiest = [i for i, count in enumerate(counts) if count == max_count]
    return busiest

Time Complexity: O(n log k), where n is the number of requests and k is the number of servers. The heapq.heappush and heapq.heappop operations take O(log k) time, and in the worst case, we might perform these operations for each request.

Space Complexity: O(k), for the available, busy, and counts arrays.

Edge Cases

  • All servers are busy: The code handles the case where all servers are busy by dropping the request and continuing to the next request.
  • Large arrival and load values: The code uses integers to store arrival and load times, which should be sufficient for the given constraints (up to 109).
  • k = 1: If there is only one server, the modulo operation will always result in 0, so every request is assigned to the same server if it's available.
  • Empty arrival/load arrays: The problem statement says the arrays have at least length 1, so we do not need to handle this.