PracHub
QuestionsPremiumLearningGuidesCheatsheetNEWCareers

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.

  • Medium
  • OpenAI
  • Coding & Algorithms
  • Software Engineer

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.

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.

You are given a sequence of GPU credit ledger operations for multiple users. Process the operations in order. Each credit addition creates a separate lot with its own expiry time. A credit lot is valid at timestamp t if and only if t < expiry_time. The operations are: ('add', user_id, amount, expiry_time) to add a lot, ('subtract', user_id, amount, timestamp) to atomically deduct credits using unexpired lots with the earliest expiry first, and ('balance', user_id, timestamp) to report the total unexpired balance and the remaining unexpired lots as (amount, expiry_time) pairs sorted by expiry. Partial consumption of a lot is allowed. If a subtract operation cannot fully deduct the requested amount from unexpired lots, it must fail and leave the ledger unchanged. If a user has no credits, its balance is 0 and subtract fails unless the amount is 0. Your task is to implement solution(operations), which returns the results of all non-add operations in order: subtract returns True/False, and balance returns (total, breakdown). Follow-up discussion: analyze time/space complexity, explain how to optimize toward O(log n) updates/queries for heavy workloads, and describe how to persist and restore 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.

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 results

Time 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

  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.
Last updated: Apr 30, 2026

Loading coding console...

PracHub

Master your tech interviews with 7,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • Careers
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Simulate Infection Spread on a Grid - OpenAI (hard)
  • Implement Social Follow Recommendations - OpenAI (medium)
  • Build a Compose Rating Card - OpenAI (medium)
  • Convert IPv4 Ranges to CIDR Blocks - OpenAI (medium)
  • Implement Persistent KV Store Serialization - OpenAI (hard)