Flight Fare Cache

HarddesignAlso known as LRU Cache with TTL

A travel site shows fares that come from slow airline APIs, so it caches each quote in memory. Two rules fight over the cache. A fare goes stale after a few minutes, so every entry has a time to live and must not be served after it runs out. And memory is limited, so when the cache is full the entry nobody has used for the longest goes first. An entry that has already expired should never push out one that is still good.

This is the in-memory cache from Caching, with TTL expiry and least-recently-used eviction, the same pair of rules an in-memory cache server such as Redis can be configured to apply (real servers approximate LRU to save memory).

Your task

Write a class FareCache. An entry stored at time s with time to live ttlMs expires at s + ttlMs: from then on it is treated as if it were not in the cache at all, and it takes no space.

  • constructor(capacity): at most capacity unexpired entries.
  • get(route, t): if route has an unexpired entry, return its fare and mark it as just used. Otherwise return -1.
  • put(route, fare, ttlMs, t): store the fare with a new expiry time and mark it as just used. If route already has an unexpired entry, replace it. If not, and the cache already holds capacity unexpired entries, first remove the one that was used least recently (by get or put).
  • size(t): return the number of unexpired entries.

class FareCache

Examples

Example 1
Input
new FareCache(2)
.put("LHR-JFK", 420, 5000, 0)
.put("CDG-NRT", 880, 5000, 100)
.get("LHR-JFK", 200)
.put("SFO-SYD", 1100, 5000, 300)
.get("CDG-NRT", 400)
.get("SFO-SYD", 400)
.size(400)
Output
[null, null, null, 420, null, -1, 1100, 2]
Why

Nothing expires before 5,000 here, so this is plain LRU. Reading LHR-JFK at 200 makes it the most recently used, which leaves CDG-NRT as the least recently used. The cache is full when SFO-SYD arrives, so CDG-NRT is evicted. The cache now holds LHR-JFK and SFO-SYD.

Example 2
Input
new FareCache(2)
.put("A", 1, 100, 0)
.put("B", 2, 1000, 10)
.get("A", 50)
.put("C", 3, 1000, 150)
.get("B", 160)
.get("A", 160)
.size(160)
Output
[null, null, null, 1, null, 2, -1, 2]
Why

After the read at 50, B is the least recently used. But A expired at 0 + 100 = 100, so at 150 the cache holds only B, and C fits without evicting anything. A cache that let A keep its slot until something pushed it out would have evicted B, the one fare still worth keeping.

Constraints

  • 1 ≤ capacity ≤ 1,000
  • 1 ≤ ttlMs ≤ 1,000,000,000
  • 1 ≤ fare ≤ 1,000,000
  • 0 ≤ t ≤ 1,000,000,000,000, and t never decreases from one call to the next
  • Routes are non-empty strings of up to 20 characters
  • At most 3,000 calls per test
Start with brute force

Store entries in a plain list with a "last used" time, and on every call scan the list to drop expired entries, find a route, or find the least recently used. Each call is O(n), so 3,000 calls on a cache of 1,000 entries is about 3 million steps here, and a real cache with a million entries serving thousands of reads a second cannot afford a full scan per read.

The ordered map makes the LRU part O(1), and the heap finds expired entries in order of expiry, so each entry is removed once, in O(log n).

Hints

0 of 4 shown

Walkthrough and solution

Unlocks when you pass every test. Stuck for a while? Looking is fine too, as long as you then close it and write the code yourself.

Ctrl+Enter runs

Run your code to see results here. Run checks the examples. Submit checks every test, including hidden edge cases.

Write code