Coding questions · OpenAI
Implement an expiring GPU credits ledger
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.
Implement an expiring GPU credits ledger for multiple users with three operations:
- add_credit(user_id, amount, expiry_time): add a lot of credits that expires at expiry_time.
- subtract(user_id, amount, timestamp): atomically deduct amount at timestamp by consuming unexpired lots in order of earliest expiry first; lots with expiry_time <= timestamp are expired and cannot be used; if total unexpired balance < amount, the operation must fail and make no changes.
- get_balance(user_id, timestamp): return the total unexpired balance at timestamp and a breakdown by remaining lots (amount, expiry_time). Assumptions: time is integer seconds; amounts are non-negative integers; partial consumption of a lot is allowed; exact expiry boundary uses the rule "valid iff timestamp < expiry_time". Tasks: implement the three functions; include tests for (a) single lot add and spend, (b) spending across multiple lots with different expiries, (c) attempts to spend after a lot expires, (d) insufficient-balance failure leaving state unchanged, (e) edge case at expiry boundary. Follow up: analyze time/space complexity; discuss how to optimize for many operations (e.g., data structures to achieve O(log n) per update) and how to persist/restore state.
Implement an Expiring GPU Credits Ledger
Implement an expiring GPU credits ledger that processes a stream of operations for multiple users and reports the results of every query.
What to implement
Implement:
def solution(operations):
...
operations is a list of operation tuples, processed in the given order. Each credit a user adds creates a separate lot with its own expiry time. A lot is valid (unexpired) at a timestamp t if and only if t < expiry_time (so a lot with expiry_time <= t is expired at t).
Return a list containing the result of every non-add operation, in the order the operations were processed. add operations produce no output.
Operations
Each tuple's first element is its type:
-
('add', user_id, amount, expiry_time)— Add a new lot ofamountcredits touser_idthat expires atexpiry_time. Produces no result. (Anaddwithamount <= 0adds nothing.) -
('subtract', user_id, amount, timestamp)— Atomically deductamountcredits fromuser_idusing only lots that are unexpired attimestamp, spending from the lot with the earliestexpiry_timefirst. Partial consumption of a lot is allowed (a lot may be left with a reduced amount). ReturnsTrueon success orFalseon failure:- If the total of
user_id's unexpired credits is less thanamount, the operation fails: returnFalseand leave the ledger unchanged (no lots are modified). - Otherwise it succeeds: deduct exactly
amountearliest-expiry-first and returnTrue. - A subtract of
amount == 0always succeeds (returnsTrue) and changes nothing, even for a user with no credits.
- If the total of
-
('balance', user_id, timestamp)— Reportuser_id's credits that are unexpired attimestamp. Returns a tuple(total, breakdown)where:totalis the sum of all unexpired lot amounts.breakdownis the list of remaining unexpired lots as(amount, expiry_time)pairs, sorted byexpiry_time(ascending).
A user with no unexpired credits has balance
(0, []).
Notes
- Expired lots never count toward a balance, can never be spent, and should be treated as gone once their
expiry_timehas been reached at the timestamp of a query. - Users are independent; an operation on one
user_idnever affects another.
Constraints
1 <= len(operations) <= 200000 <= amount <= 10^90 <= expiry_time, timestamp <= 10^9- Operations are processed in the given order.
- A lot is usable exactly when
timestamp < expiry_time. - The
timestampvalues of the non-addoperations (subtractandbalance) are non-decreasing in processing order (time only moves forward across queries).addoperations carry no timestamp and may specify anyexpiry_time.
Follow-up
Analyze the time and space complexity of your approach. Discuss how to optimize toward O(log n) updates and queries for heavy workloads, and describe how you would persist and restore the ledger state.
Examples
Example 1
Input: ([('add', 'u1', 50, 10), ('balance', 'u1', 5), ('subtract', 'u1', 20, 6), ('balance', 'u1', 6)],)
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.
Example 2
Input: ([('add', 'alice', 10, 5), ('add', 'alice', 30, 20), ('subtract', 'alice', 10, 5), ('balance', 'alice', 5)],)
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.
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.
Approach
Idea. Each credit "lot" must be spent earliest-expiry-first, so per user we keep a min-heap keyed by [expiry_time, insertion_order, amount]. insertion_order is just a tiebreaker so heap entries never compare on the mutable amount. A parallel totals[user] caches the sum of all live lots so balance/feasibility checks are O(1).
prune(user, t) pops every lot at the heap top with expiry_time <= t, decrementing totals. Because a lot is valid iff t < expiry_time, expiry_time <= t means expired. Pruning only touches the cheapest-expiry lots, which are exactly the ones that die first — so the heap order makes expiry removal correct and cheap.
add pushes [expiry, order, amount] and bumps totals (skipping non-positive amounts, and skipping lots already expired relative to the last seen timestamp as an optimization).
subtract(user, amount, t) first prunes at t. If amount == 0 it trivially succeeds. Otherwise it checks totals[user] >= amount before mutating anything — this is what guarantees atomicity: a request that can't be fully covered returns False and leaves the ledger untouched. When it can succeed, it pops lots earliest-expiry-first, fully consuming each until the last, which is partially consumed and pushed back with its reduced amount.
balance(user, t) prunes then returns (totals[user], sorted lots as (amount, expiry) pairs).
Why correct: earliest-expiry-first is optimal because spending a soon-to-expire credit can never reduce future availability versus spending a longer-lived one; the pre-check makes failure side-effect-free; and pruning at every query keeps reported balances expiry-accurate.
- Space complexity: O(M), where M is the total number of unexpired lots stored across all users at once (one heap entry plus one totals counter per active lot/user).