Quick Overview

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

Company: OpenAI

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

Implement an expiring GPU credits ledger for multiple users with three operations: 1) add_credit(user_id, amount, expiry_time): add a lot of credits that expires at expiry_time. 2) 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. 3) 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.

Overview: 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.

Read the full OpenAI Software Engineer interview experience this question came from

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: ```python 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 of `amount` credits to `user_id` that expires at `expiry_time`. Produces no result. (An `add` with `amount <= 0` adds nothing.) - **`('subtract', user_id, amount, timestamp)`** — Atomically deduct `amount` credits from `user_id` using only lots that are unexpired at `timestamp`, spending from the lot with the **earliest `expiry_time` first**. **Partial consumption of a lot is allowed** (a lot may be left with a reduced amount). Returns **`True`** on success or **`False`** on failure: - If the total of `user_id`'s unexpired credits is less than `amount`, the operation **fails**: return `False` and leave the ledger **unchanged** (no lots are modified). - Otherwise it succeeds: deduct exactly `amount` earliest-expiry-first and return `True`. - A subtract of `amount == 0` always **succeeds** (returns `True`) and changes nothing, even for a user with no credits. - **`('balance', user_id, timestamp)`** — Report `user_id`'s credits that are unexpired at `timestamp`. Returns a tuple `(total, breakdown)` where: - `total` is the sum of all unexpired lot amounts. - `breakdown` is the list of remaining unexpired lots as `(amount, expiry_time)` pairs, **sorted by `expiry_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_time` has been reached at the timestamp of a query. - Users are independent; an operation on one `user_id` never affects another. ## 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`. - The `timestamp` values of the **non-`add` operations** (`subtract` and `balance`) are **non-decreasing** in processing order (time only moves forward across queries). `add` operations carry no timestamp and may specify any `expiry_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.

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.

Hints

  1. Store each user's credits as separate lots instead of merging everything into one number, because expiry times matter during subtraction.
  2. For atomic subtraction, first compute whether enough unexpired balance exists before changing any lot.

Loading coding console...

Show the approach

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).