Build the full in-memory database with backups. Process operations in chronological order. Support put(key, value, t) for non-expiring values, put(key, value, t, ttlSeconds) for expiring values, get(key, t), scan(prefix, t), backup(t), and restore(backupId, t). A backup captures exactly the non-expired database state at time t and returns a new integer backup id starting from 1. Restore replaces the current database with the saved state from that backup. If an item had remaining TTL at backup time, it should expire that many seconds after restore, not receive a fresh full TTL. Represent scan results as [key, value] pairs sorted lexicographically by key.
Examples
Input: [['put', 'a', 'x', 1], ['backup', 2], ['put', 'a', 'y', 3], ['get', 'a', 4], ['restore', 1, 5], ['get', 'a', 6]]
Expected Output: [1, 'y', 'x']
Explanation: After restore, the database returns to the exact state saved in backup 1.
Input: [['put', 'temp', 'v', 1, 10], ['backup', 5], ['restore', 1, 20], ['get', 'temp', 25], ['get', 'temp', 26]]
Expected Output: [1, 'v', None]
Explanation: At backup time 5, the key has 6 seconds remaining. After restore at 20, it expires at 26.
Input: [['put', 'a', '1', 1, 2], ['put', 'b', '2', 2], ['backup', 4], ['restore', 1, 10], ['scan', '', 11]]
Expected Output: [1, [['b', '2']]]
Explanation: Key 'a' is already expired at backup time, so it is not saved.
Input: [['backup', 1], ['put', 'x', '1', 2], ['restore', 1, 3], ['get', 'x', 4], ['scan', '', 4]]
Expected Output: [1, None, []]
Explanation: Edge case: restoring an empty backup clears the database.
Solution
def solution(operations):
from bisect import bisect_left
state = {}
keys = []
backups = {}
next_backup_id = 1
out = []
def alive(entry, t):
if entry is None:
return False
_, expire_at = entry
return expire_at is None or t < expire_at
for op in operations:
cmd = op[0]
if cmd == 'put':
if len(op) == 5:
_, key, value, t, ttl = op
expire_at = t + ttl
else:
_, key, value, t = op
expire_at = None
if key not in state:
i = bisect_left(keys, key)
keys.insert(i, key)
state[key] = (value, expire_at)
elif cmd == 'get':
_, key, t = op
entry = state.get(key)
out.append(entry[0] if alive(entry, t) else None)
elif cmd == 'scan':
_, prefix, t = op
i = bisect_left(keys, prefix)
cur = []
while i < len(keys) and keys[i].startswith(prefix):
k = keys[i]
entry = state.get(k)
if alive(entry, t):
cur.append([k, entry[0]])
i += 1
out.append(cur)
elif cmd == 'backup':
_, t = op
snapshot = {}
snap_keys = []
for k in keys:
entry = state.get(k)
if alive(entry, t):
value, expire_at = entry
remaining = None if expire_at is None else expire_at - t
snapshot[k] = (value, remaining)
snap_keys.append(k)
backup_id = next_backup_id
next_backup_id += 1
backups[backup_id] = (snapshot, snap_keys)
out.append(backup_id)
else: # restore
_, backup_id, t = op
snapshot, snap_keys = backups[backup_id]
state = {}
keys = snap_keys[:]
for k in keys:
value, remaining = snapshot[k]
expire_at = None if remaining is None else t + remaining
state[k] = (value, expire_at)
return outTime complexity: put: O(n) worst-case for inserting a new key into the sorted list, get: O(1) average, scan: O(log n + m), backup: O(n), restore: O(s) where s is the snapshot size.. Space complexity: O(n + B), where n is the current number of keys and B is the total size of all stored backups..