Implement an expiring GPU credits ledger
Company: OpenAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates understanding of time-based resource accounting, design of data structures for expiring balances, atomic update semantics for concurrent operations, and algorithmic time/space complexity within the Coding & Algorithms domain.
Constraints
- 1 <= len(operations) <= 20000
- 0 <= amount <= 10^9
- 0 <= expiry_time, timestamp <= 10^9
- Operations are processed in the given order
- A lot is usable exactly when timestamp < expiry_time
Examples
Input: ([('add', 'u1', 50, 10), ('balance', 'u1', 5), ('subtract', 'u1', 20, 6), ('balance', 'u1', 6)],)
Expected Output: [(50, [(50, 10)]), True, (30, [(30, 10)])]
Explanation: At time 5, the lot is still valid, so the balance is 50. Subtracting 20 at time 6 succeeds, leaving 30 in the same lot.
Input: ([('add', 'alice', 10, 5), ('add', 'alice', 30, 20), ('subtract', 'alice', 10, 5), ('balance', 'alice', 5)],)
Expected Output: [True, (20, [(20, 20)])]
Explanation: A lot is valid only when `timestamp < expiry_time`, so the lot expiring at 5 is already invalid at time 5. The subtraction uses the 30-credit lot expiring at 20, leaving 20.
Input: ([('add', 'u', 5, 4), ('add', 'u', 7, 10), ('subtract', 'u', 10, 4), ('balance', 'u', 4)],)
Expected Output: [False, (7, [(7, 10)])]
Explanation: At time 4, the lot with expiry 4 is expired. Only 7 valid credits remain, so subtracting 10 fails and the ledger stays unchanged.
Input: ([('add', 'bob', 10, 20), ('add', 'alice', 8, 15), ('add', 'bob', 5, 8), ('balance', 'bob', 7), ('subtract', 'bob', 6, 7), ('balance', 'bob', 7), ('balance', 'alice', 7)],)
Expected Output: [(15, [(5, 8), (10, 20)]), True, (9, [(9, 20)]), (8, [(8, 15)])]
Explanation: Bob's balance at time 7 includes both lots, sorted by expiry. Subtracting 6 spends 5 from the lot expiring at 8 and 1 from the lot expiring at 20. Alice's credits are unaffected.
Input: ([('subtract', 'ghost', 0, 3), ('balance', 'ghost', 3), ('subtract', 'ghost', 1, 3)],)
Expected Output: [True, (0, []), False]
Explanation: Subtracting 0 always succeeds, even for a user with no credits. The user's balance is 0, and subtracting 1 then fails.
Input: ([('add', 'u', 5, 10), ('add', 'u', 7, 10), ('subtract', 'u', 6, 1), ('balance', 'u', 1)],)
Expected Output: [True, (6, [(6, 10)])]
Explanation: Both lots have the same expiry, so the earlier-added lot is spent first. The first 5-credit lot is fully consumed, and 1 credit is taken from the second lot, leaving 6.
Solution
def solution(operations):
import heapq
from collections import defaultdict
heaps = defaultdict(list) # user_id -> min-heap of [expiry_time, insertion_order, amount]
totals = defaultdict(int) # user_id -> total unexpired amount currently tracked
results = []
insertion_order = 0
current_time = None
def prune(user_id, timestamp):
heap = heaps[user_id]
while heap and heap[0][0] <= timestamp:
expiry_time, order, amount = heapq.heappop(heap)
totals[user_id] -= amount
for op in operations:
kind = op[0]
if kind == 'add':
_, user_id, amount, expiry_time = op
if amount <= 0:
continue
# Because non-add timestamps are nondecreasing and operations are processed
# in order, a lot added after time has advanced past its expiry is useless.
if current_time is not None and expiry_time <= current_time:
continue
heapq.heappush(heaps[user_id], [expiry_time, insertion_order, amount])
totals[user_id] += amount
insertion_order += 1
elif kind == 'subtract':
_, user_id, amount, timestamp = op
current_time = timestamp
prune(user_id, timestamp)
if amount == 0:
results.append(True)
continue
if totals[user_id] < amount:
results.append(False)
continue
totals[user_id] -= amount
remaining_to_spend = amount
heap = heaps[user_id]
while remaining_to_spend > 0:
expiry_time, order, lot_amount = heapq.heappop(heap)
if lot_amount <= remaining_to_spend:
remaining_to_spend -= lot_amount
else:
lot_amount -= remaining_to_spend
remaining_to_spend = 0
heapq.heappush(heap, [expiry_time, order, lot_amount])
results.append(True)
elif kind == 'balance':
_, user_id, timestamp = op
current_time = timestamp
prune(user_id, timestamp)
breakdown = [
(amount, expiry_time)
for expiry_time, order, amount in sorted(heaps[user_id])
]
results.append((totals[user_id], breakdown))
else:
raise ValueError('Unknown operation type: ' + str(kind))
return resultsTime complexity: Add: O(log m). Subtract: O((p + c) log m), where p is the number of newly pruned expired lots and c is the number of lots consumed. Balance: O(m log m) to sort the active lots for that user. Here m is the number of active lots for the queried user.. Space complexity: O(M), where M is the total number of unexpired lots currently stored across all users..
Hints
- Store each user's credits as separate lots instead of merging everything into one number, because expiry times matter during subtraction.
- For atomic subtraction, first compute whether enough unexpired balance exists before changing any lot.