Compute courier pay and implement load balancing
Company: DoorDash
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This problem evaluates skills in interval arithmetic and aggregation for time-based pay computation alongside routing and load-balancing knowledge covering round-robin and consistent hashing with virtual nodes, testing time-interval handling, rate mapping, hashing, and data-structure design for high-volume inputs.
Part 1: Compute Courier Pay
Constraints
- 0 <= start_minute < end_minute <= 1440
- 0 <= pickup_minute <= dropoff_minute <= 1440
- Rate intervals do not overlap, but they may leave gaps in the day
- 1 <= len(rate_intervals), len(trips) <= 100000 is possible, but the day length is fixed at 1440 minutes
- rate_per_minute and per_trip_bonus are non-negative integers
Examples
Input: ([(0, 60, 2), (60, 120, 1)], [(30, 90, 5)])
Expected Output: 95
Explanation: From minute 30 to 60 the rate is 2 for 30 minutes, and from 60 to 90 the rate is 1 for 30 minutes. Total time pay is 60 + 30, plus bonus 5.
Input: ([(0, 100, 1), (200, 300, 2)], [(50, 250, 10), (260, 280, 0)])
Expected Output: 200
Explanation: First trip pays 50*1 + 100*0 + 50*2 + 10 = 160. Second trip pays 20*2 = 40. Combined total is 200.
Input: ([], [(10, 20, 3)])
Expected Output: 3
Explanation: No pay-rate intervals exist, so time-based pay is 0. Only the trip bonus is earned.
Input: ([(0, 1440, 4)], [])
Expected Output: 0
Explanation: There are no trips, so total pay is 0.
Solution
def solution(rate_intervals, trips):
diff = [0] * 1442
for start, end, rate in rate_intervals:
diff[start] += rate
diff[end] -= rate
per_minute = [0] * 1440
running = 0
for minute in range(1440):
running += diff[minute]
per_minute[minute] = running
prefix_pay = [0] * 1441
for minute in range(1440):
prefix_pay[minute + 1] = prefix_pay[minute] + per_minute[minute]
total = 0
for pickup, dropoff, bonus in trips:
total += prefix_pay[dropoff] - prefix_pay[pickup] + bonus
return totalTime complexity: O(len(rate_intervals) + len(trips) + 1440). Space complexity: O(1440).
Hints
- Because the day has only 1440 minutes, you can precompute the pay rate for every minute of the day.
- After building per-minute rates, a prefix-sum array lets you answer each trip's time-based pay in O(1).
Part 2: Implement Request Routing with Round Robin
Constraints
- 1 <= len(operations) <= 200000
- serverId and requestId are non-empty strings
- Servers should be routed in the order they were added among currently active servers
- Duplicate addServer operations are ignored
- removeServer on a missing server is ignored
Examples
Input: ([('addServer', 'A'), ('addServer', 'B'), ('route', 'r1'), ('route', 'r2'), ('route', 'r3')],)
Expected Output: ['A', 'B', 'A']
Explanation: With servers A and B active, routing cycles A -> B -> A.
Input: ([('addServer', 'A'), ('addServer', 'B'), ('addServer', 'C'), ('route', 'x'), ('removeServer', 'B'), ('route', 'y'), ('route', 'z')],)
Expected Output: ['A', 'C', 'A']
Explanation: After routing to A, the next server would have been B. Removing B moves the cycle forward to C, then back to A.
Input: ([('route', 'r1'), ('addServer', 'X'), ('removeServer', 'X'), ('route', 'r2')],)
Expected Output: [None, None]
Explanation: There is no active server for either route call.
Input: ([('addServer', 'A'), ('addServer', 'A'), ('addServer', 'B'), ('route', 'p'), ('addServer', 'C'), ('removeServer', 'Z'), ('route', 'q'), ('route', 'r'), ('route', 's')],)
Expected Output: ['A', 'B', 'C', 'A']
Explanation: The second add of A is ignored, removing Z does nothing, and the cycle continues through A, B, C.
Solution
def solution(operations):
class Node:
__slots__ = ('sid', 'prev', 'next')
def __init__(self, sid):
self.sid = sid
self.prev = self
self.next = self
nodes = {}
tail = None
cursor = None
result = []
for op in operations:
action = op[0]
if action == 'addServer':
sid = op[1]
if sid in nodes:
continue
node = Node(sid)
nodes[sid] = node
if tail is None:
tail = node
cursor = node
else:
head = tail.next
node.prev = tail
node.next = head
tail.next = node
head.prev = node
tail = node
elif action == 'removeServer':
sid = op[1]
node = nodes.pop(sid, None)
if node is None:
continue
if node.next is node:
tail = None
cursor = None
continue
if cursor is node:
cursor = node.next
node.prev.next = node.next
node.next.prev = node.prev
if tail is node:
tail = node.prev
else: # route
if cursor is None:
result.append(None)
else:
result.append(cursor.sid)
cursor = cursor.next
return resultTime complexity: O(1) average time per addServer, removeServer, and route operation. Space complexity: O(S), where S is the number of active servers.
Hints
- Think about keeping a pointer to the next server that should receive traffic.
- A hash map plus a circular doubly linked list gives O(1) add, remove, and route.
Part 3: Implement Request Routing with Consistent Hashing
Constraints
- 1 <= len(operations) <= 100000
- 1 <= virtual_nodes <= 1000
- serverId and requestId are non-empty strings
- Use the exact hash function and ring rule described in the statement so routing is deterministic
- Duplicate addServer operations are ignored, and removeServer on a missing server is ignored
Examples
Input: ([('addServer', 'A'), ('addServer', 'B'), ('route', 'Ne'), ('route', 'Oe'), ('route', 'Qe'), ('route', 'zz')], 2)
Expected Output: ['B', 'A', 'B', 'A']
Explanation: With 2 virtual nodes each, the ring positions are A#0=279, B#0=280, A#1=282, B#1=283. The request hashes are Ne=280, Oe=281, Qe=283, zz=366, so routing is B, A, B, then wrap to A.
Input: ([('addServer', 'A'), ('addServer', 'B'), ('route', 'Ne'), ('removeServer', 'B'), ('route', 'Ne'), ('route', 'Oe')], 2)
Expected Output: ['B', 'A', 'A']
Explanation: Before removal, Ne hashes to B. After removing B, both Ne=280 and Oe=281 route to A's next virtual node at 282.
Input: ([('addServer', 'A'), ('route', 'Ne'), ('addServer', 'B'), ('route', 'Ne'), ('route', 'Pe')], 2)
Expected Output: ['A', 'B', 'A']
Explanation: With only A active, Ne routes to A. After adding B, Ne remaps to B's virtual node at 280, while Pe=282 still maps to A.
Input: ([('route', 'x'), ('addServer', 'A'), ('removeServer', 'A'), ('route', 'y')], 3)
Expected Output: [None, None]
Explanation: There are no active servers at either route call.
Solution
def solution(operations, virtual_nodes):
from bisect import bisect_left, insort
def ring_hash(s):
total = 0
for i, ch in enumerate(s):
total += (i + 1) * ord(ch)
return total
ring = []
server_to_nodes = {}
result = []
for op in operations:
action = op[0]
if action == 'addServer':
sid = op[1]
if sid in server_to_nodes:
continue
nodes = []
for i in range(virtual_nodes):
entry = (ring_hash(f'{sid}#{i}'), sid, i)
insort(ring, entry)
nodes.append(entry)
server_to_nodes[sid] = nodes
elif action == 'removeServer':
sid = op[1]
nodes = server_to_nodes.pop(sid, None)
if not nodes:
continue
for entry in nodes:
idx = bisect_left(ring, entry)
if idx < len(ring) and ring[idx] == entry:
ring.pop(idx)
else: # route
request_id = op[1]
if not ring:
result.append(None)
continue
key = (ring_hash(request_id), '', -1)
idx = bisect_left(ring, key)
if idx == len(ring):
idx = 0
result.append(ring[idx][1])
return resultTime complexity: Routing: O(log M). Adding or removing one server: O(V * M) worst-case with Python list insertions/removals, where V is virtual_nodes and M is the current number of virtual nodes on the ring.. Space complexity: O(M), where M is the number of active virtual nodes.
Hints
- Store all virtual nodes in a sorted structure by hash so you can binary-search the destination for each request.
- Keep track of which virtual-node entries belong to each server so removal is straightforward.