Implement account scheduler with locking and LRU
Company: Stripe
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates understanding of time-based resource scheduling, locking semantics, LRU eviction policies, and stateful data-structure management in the coding & algorithms domain.
Constraints
- All account IDs are integers; accounts is a fixed set provided at construction.
- locked_until maps account_id -> integer timestamp; accounts absent from it default to 0.
- All timestamps t are non-negative integers and are supplied in non-decreasing order across calls.
- duration is a positive integer.
- is_available is true exactly when t >= locked_until[account_id].
- acquire fails (returns False) when the account is not available at time t.
- auto_acquire returns None when no account is available at time t.
- Calls are sequential; no concurrency handling is required.
Examples
Input: ([1, 2, 3, 4], {1: 10, 2: 5, 3: 0, 4: 20}, [["is_available", 1, 8], ["is_available", 2, 8], ["is_available", 3, 1], ["is_available", 4, 21]])
Expected Output: [False, True, True, True]
Explanation: Part 1 basic queries: account 1 is locked until 10 so unavailable at 8; account 2 unlocks at 5 (available at 8); account 3 default-available; account 4 unlocks at 20 (available at 21).
Input: ([1, 2], {}, [["is_available", 1, 0], ["acquire", 1, 0, 5], ["is_available", 1, 3], ["is_available", 1, 5], ["acquire", 2, 0, 3]])
Expected Output: [True, True, False, True, True]
Explanation: Part 2: account 1 starts available; acquiring it at t=0 for 5 locks it until 5, so it is unavailable at 3 but available again at 5 (boundary is inclusive). Account 2 is independently available and acquired.
Input: ([1, 2, 3], {}, [["acquire", 1, 0, 100], ["auto_acquire", 5, 10], ["auto_acquire", 5, 10], ["auto_acquire", 5, 10]])
Expected Output: [True, 2, 3, None]
Explanation: Part 3: account 1 is locked until 100. auto_acquire at t=5 picks among {2,3} (both never used) the smaller id 2, then 3; the third call finds nothing available and returns None.
Input: ([1, 2, 3], {}, [["acquire", 1, 0, 2], ["acquire", 2, 1, 2], ["auto_acquire", 10, 1], ["auto_acquire", 10, 1], ["auto_acquire", 10, 1]])
Expected Output: [True, True, 3, 1, 2]
Explanation: LRU ordering: by t=10 all are available. last_used is {1:0, 2:1}, 3 never used. auto_acquire picks 3 (never used) first, then 1 (oldest use at 0), then 2 (used at 1).
Input: ([7], {7: 50}, [["is_available", 7, 49], ["acquire", 7, 49, 5], ["auto_acquire", 49, 1]])
Expected Output: [False, False, None]
Explanation: Account 7 is locked until 50, so it is unavailable at 49; the acquire at t=49 therefore fails (returns False) and leaves state unchanged, so auto_acquire at 49 finds nothing and returns None.
Input: ([], {}, [["auto_acquire", 0, 5]])
Expected Output: [None]
Explanation: Edge case: with no accounts in the system, auto_acquire has nothing to select and returns None.
Input: ([5], {}, [["acquire", 5, 0, 3], ["acquire", 5, 1, 10], ["is_available", 5, 2], ["is_available", 5, 3], ["auto_acquire", 3, 1]])
Expected Output: [True, False, False, True, 5]
Explanation: First acquire locks account 5 until 3. The second acquire at t=1 fails because 5 is not available then, so it does NOT extend the lock. Account 5 becomes available again at t=3 (boundary inclusive) and auto_acquire at t=3 selects it.
Solution
def solution(accounts, locked_until, operations):
# Stateful AccountScheduler driven by a list of operations.
# locked[a] = timestamp until which account a is unavailable (available for t >= locked[a]).
locked = {}
for a in accounts:
locked[a] = 0
for a, ts in locked_until.items():
locked[a] = ts
# last_used[a] = last time account a was successfully acquired.
# Never-acquired accounts are absent and treated as the oldest (preferred by LRU).
last_used = {}
def is_available(account_id, t):
return t >= locked.get(account_id, 0)
def acquire(account_id, t, duration):
if not is_available(account_id, t):
return False
locked[account_id] = max(locked.get(account_id, 0), t) + duration
last_used[account_id] = t
return True
def auto_acquire(t, duration):
best = None
best_key = None
for a in accounts:
if not is_available(a, t):
continue
used = last_used.get(a, None)
# key ordering: never-used (flag 0) before used (flag 1),
# then by oldest last-used time, then smallest account id.
if used is None:
key = (0, 0, a)
else:
key = (1, used, a)
if best_key is None or key < best_key:
best_key = key
best = a
if best is None:
return None
locked[best] = max(locked.get(best, 0), t) + duration
last_used[best] = t
return best
results = []
for op in operations:
name = op[0]
if name == "is_available":
results.append(is_available(op[1], op[2]))
elif name == "acquire":
results.append(acquire(op[1], op[2], op[3]))
elif name == "auto_acquire":
results.append(auto_acquire(op[1], op[2]))
else:
results.append(None)
return resultsTime complexity: is_available and acquire are O(1). auto_acquire is O(n) per call where n = number of accounts (a linear scan for the LRU-available pick). With m operations the driver runs in O(m + (a_calls * n)); a heap/ordered structure keyed by last-used could reduce auto_acquire toward O(log n) but must still skip currently-locked entries.. Space complexity: O(n) for the locked and last_used maps over n accounts, plus O(m) for the results list..
Hints
- Keep a single dict locked_until[account_id]; availability at time t is simply t >= locked_until.get(account_id, 0).
- For acquire, the new lock end is max(locked_until[account_id], t) + duration so an already-future lock extends correctly rather than being shortened.
- Track a separate last_used map updated only on a *successful* acquire (explicit or auto). Never-acquired accounts should compare as older than any used account.
- For auto_acquire, scan available accounts and pick the LRU using the tuple key (was_used?, last_used_time, account_id) so never-used wins first, then oldest, then smallest id breaks ties deterministically.