13. Assign Tickets by SLA Deadline
Problem
A support-ticket system fields tickets each carrying an SLA deadline; the soonest deadline should always be handled next, regardless of arrival order. Given a stream of tickets and a fixed number of available agents, process tickets in strict deadline order using a min-heap, assigning agents round-robin in the order tickets are handled (not the order they arrived).
tickets is a list of (ticket_id, deadline), NOT pre-sorted. The function returns a list of (ticket_id, agent_index) in handling order: soonest deadline first, ties broken by original input (list) order; agent_index cycles 0..num_agents-1 in that handling order.
Source: src/13_assign_tickets_by_deadline.py
def assign_by_deadline(tickets: list[tuple[str, float]], num_agents: int) -> list[tuple[str, int]]: ...
>>> assign_by_deadline([("a", 5.0), ("b", 1.0), ("c", 3.0)], num_agents=2)
[('b', 0), ('c', 1), ('a', 0)]
>>> assign_by_deadline([("a", 2.0), ("b", 2.0), ("c", 1.0)], num_agents=3)
[('c', 0), ('a', 1), ('b', 2)]
Step-by-Step Approach
- Build a min-heap keyed on
(deadline, original_index, ticket_id). Includingoriginal_indexboth breaks ties deterministically (earliest-arrival-first among equal deadlines) and avoids ever comparing two tuples onticket_idif deadlines and indices happen to tie (they can't, since index is unique). - Heapify the list of all tickets up front in O(n) — you don't need to push one at a time since the whole ticket list is known ahead of time.
- Pop from the heap repeatedly; each pop gives you the ticket with the globally next-soonest deadline, which is exactly the "handling order."
- Maintain a running
agentcounter starting at 0. After handling each ticket, append(ticket_id, agent)to the result and advanceagent = (agent + 1) % num_agents. - Continue until the heap is empty; return the accumulated result list.
- Handle the empty-tickets case by returning an empty list directly (heapify on an empty list is a no-op, so this falls out naturally).
The key insight is that "assign agents round-robin in handling order" decouples entirely from "sort by deadline" — you first fully resolve the deadline-priority order via the heap, and only then do a simple modulo counter over that resolved order, rather than trying to interleave the two concerns.
Reference solution
import heapq
def assign_by_deadline(tickets: list[tuple[str, float]], num_agents: int) -> list[tuple[str, int]]:
# enumerate pairs each ticket with its index; idx breaks deadline ties before ticket_id ever compares
heap = [(deadline, idx, ticket_id) for idx, (ticket_id, deadline) in enumerate(tickets)]
# O(n) in-place heap build, cheaper than n individual pushes
heapq.heapify(heap)
result: list[tuple[str, int]] = []
agent = 0
while heap:
# O(log n) pop of smallest; _, _ discards deadline/idx
_, _, ticket_id = heapq.heappop(heap)
result.append((ticket_id, agent))
# cycles 0..num_agents-1
agent = (agent + 1) % num_agents
return result
Key Functions & Tricks
heapq.heapify(list)— O(n) in-place heap build, cheaper than pushing each ticket one at a time.heapq.heappop(heap)— O(log n) pop of the smallest element; Python'sheapqis min-heap only.enumerate(tickets)in a list comprehension — pairs each ticket with its original index in one line.(deadline, idx, ticket_id)tuple ordering —idxtie-breaks deadlines soticket_idstrings are never compared._, _, ticket_id = heapq.heappop(heap)— underscore convention discards tuple elements you don't need.agent = (agent + 1) % num_agents— modulo cycles the round-robin agent index.
How to Recognize This Pattern
Reach for a heap/priority-queue whenever a problem needs "process items in priority order" from an unsorted collection, especially when you'd otherwise be tempted to re-sort or re-scan on every step — a heap gives O(log n) extraction instead of repeated O(n) scans. The round-robin agent assignment layered on top is a separate, much simpler pattern (a modulo counter) that's easy to conflate with the priority logic; keep them as two clearly separate steps. A common variation is a streaming version where tickets arrive over time (push onto the heap as they arrive, pop when an agent frees up) rather than being fully known upfront. A common pitfall is comparing raw (deadline, ticket_id) tuples in the heap when ticket_id is a string — if two tickets ever share a deadline, Python will fall back to comparing ticket_id strings for tie-breaking (arbitrary alphabetical order) instead of preserving arrival order, unless you explicitly include the original index as the tie-breaker.