Implement expiring credit ledger
Company: OpenAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates the ability to design efficient time-based data structures and algorithms for managing expiring credits with ordering and tie-breaking rules, testing competencies in event ordering, interval management, and retroactive update handling; it is in the Coding & Algorithms domain and emphasizes practical application of algorithmic and data-structure reasoning. It is commonly asked to verify correctness under out-of-order inserts and queries, to assess performance and complexity analysis for add/charge/get operations, and to require clear specification of time and space complexity for the proposed approach.
Constraints
- 1 <= len(operations) <= 2 * 10^5
- 0 <= amount, time_stamp, expiration <= 10^9
- All `id` values in `add` operations are distinct strings
- A credit is active on [start, end), so it is active at `start` but not active at `end`
- Use 64-bit integer arithmetic in static languages
Examples
Input: [('charge', 45, 6), ('add', 'A', 50, 5, 10), ('balance', 8), ('charge', 15, 7), ('balance', 6), ('add', 'B', 30, 3, 4)]
Expected Output: [45, 20, 15, 35]
Explanation: Evaluate by time: add B at 3, add A at 5, charge 45 at 6 consumes B=30 then A=15, so the balance at time 6 is 35. At time 7, B is expired and charge 15 consumes from A, leaving 20 for the balance at time 8. Outputs are returned in original input order.
Input: [('balance', 1), ('charge', 5, 4), ('charge', 4, 2), ('balance', 3), ('add', 'C', 4, 2, 1)]
Expected Output: [0, 0, 4, 0]
Explanation: At time 1 there is no credit, so balance is 0. At time 2, the add happens before the charge, so the charge gets 4. The bucket is active on [2, 3), so by time 3 it is no longer active, and the later charge at time 4 also returns 0.
Input: [('balance', 5), ('charge', 3, 5), ('add', 'Z', 10, 5, 0)]
Expected Output: [0, 0]
Explanation: A bucket with expiration 0 is active on the empty interval [5, 5), so it is never usable. At timestamp 5, add is considered first, then charge, then balance, but both output-producing operations still see 0.
Input: [('balance', 10), ('add', 'x', 10, 5, 5), ('charge', 10, 11), ('add', 'y', 8, 1, 10), ('charge', 12, 6)]
Expected Output: [6, 0, 12]
Explanation: At time 6, charge 12 uses x first because it expires earlier, then uses part of y, leaving 6 in y. At time 10, x is expired so balance is 6. At time 11, y is also expired, so the charge returns 0.
Input: []
Expected Output: []
Explanation: No operations means no outputs.
Solution
def solution(operations):
import heapq
events = []
output_index = {}
next_output = 0
for idx, op in enumerate(operations):
kind = op[0]
if kind == 'add':
_, bucket_id, amount, time_stamp, expiration = op
events.append((time_stamp, 0, idx, bucket_id, amount, expiration))
elif kind == 'charge':
_, amount, time_stamp = op
events.append((time_stamp, 1, idx, amount))
output_index[idx] = next_output
next_output += 1
elif kind == 'balance':
_, time_stamp = op
events.append((time_stamp, 2, idx))
output_index[idx] = next_output
next_output += 1
else:
raise ValueError('Unknown operation type')
events.sort()
results = [0] * next_output
# Heap item: [expiration_time, activation_time, id, serial, remaining]
heap = []
total = 0
serial = 0
def clean(current_time):
nonlocal total
while heap and (heap[0][0] <= current_time or heap[0][4] == 0):
expiration, activation, bucket_id, order, remaining = heapq.heappop(heap)
if expiration <= current_time and remaining > 0:
total -= remaining
for event in events:
time_stamp = event[0]
kind_order = event[1]
original_idx = event[2]
if kind_order == 0:
bucket_id, amount, expiration = event[3], event[4], event[5]
if amount <= 0 or expiration <= 0:
continue
item = [time_stamp + expiration, time_stamp, bucket_id, serial, amount]
serial += 1
heapq.heappush(heap, item)
total += amount
elif kind_order == 1:
need = event[3]
if need < 0:
need = 0
clean(time_stamp)
charged = 0
while need > 0 and heap:
bucket = heap[0]
take = min(need, bucket[4])
bucket[4] -= take
need -= take
charged += take
total -= take
if bucket[4] == 0:
heapq.heappop(heap)
results[output_index[original_idx]] = charged
else:
clean(time_stamp)
results[output_index[original_idx]] = total
return resultsTime complexity: O(n log n). Space complexity: O(n).
Hints
- Think of this as a sweep-line problem: sort all operations by effective execution order on the timeline.
- Maintain the currently active credit buckets in a min-heap keyed by (expiration_end, activation_time, id), and keep a running sum of active remaining credit.