Fixed-Capacity Cache That Evicts the Least Frequently Used Key, Ties by Recency
Company: Alibaba
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Implement a key-value cache with a fixed capacity that, when it is full and a new key has to be inserted, evicts the key that has been used the fewest times. If several keys share the smallest use count, it evicts the one among them that was used least recently.
The cache is tested through one function: it receives the capacity and a sequence of operations, applies them in order to an initially empty cache, and returns the results of the read operations.
### Function Signature
```python
def simulate_lfu_cache(capacity: int, operations: list[str], arguments: list[list[int]]) -> list[int]:
```
`operations[i]` is `"get"` or `"put"`. For `"get"`, `arguments[i]` is `[key]`; for `"put"`, it is `[key, value]`.
### Rules
- Every key in the cache has a use count.
- `get(key)`: if `key` is in the cache, the result is its value, its use count increases by 1, and it becomes the most recently used key. Otherwise the result is `-1` and the cache does not change.
- `put(key, value)`: if `key` is already in the cache, its value is replaced, its use count increases by 1, and it becomes the most recently used key; nothing is evicted. Otherwise, if the cache already holds `capacity` keys, one key is evicted first: the key with the smallest use count, and among keys tied for the smallest count, the least recently used one. Then `key` is inserted with `value` and a use count of 1, and it becomes the most recently used key.
- An evicted key loses its count. If it is inserted again later, its count starts again at 1.
- Return one integer per `"get"` operation, in the order the operations appear. `"put"` operations produce no output.
- Each operation must run in $O(1)$ average time.
### Constraints
- `1 <= capacity <= 10^4`
- `1 <= len(operations) <= 2 * 10^5`, and `len(arguments) == len(operations)`
- `0 <= key <= 10^5`
- `0 <= value <= 10^9`
- Every operation is `"get"` or `"put"`, with the matching number of arguments.
### Examples
**Example 1**
```text
Input: capacity = 3,
operations = ["put", "put", "put", "get", "get", "put", "get", "get", "put", "get", "put", "put", "get", "get", "get"],
arguments = [[1, 100], [2, 200], [3, 300], [2], [3], [4, 400], [1], [4], [5, 500], [2], [3, 333], [6, 600], [5], [3], [6]]
Output: [200, 300, -1, 400, -1, -1, 333, 600]
```
Key 1 is the only key never read, so `put(4, 400)` evicts it. Just before `put(5, 500)`, keys 2, 3 and 4 all have a use count of 2, and key 2 was used least recently, so it is evicted. `put(3, 333)` updates key 3 and raises its count to 3. Just before `put(6, 600)`, key 5 has the smallest count, 1, and is evicted.
**Example 2**
```text
Input: capacity = 1,
operations = ["put", "get", "put", "get", "get"],
arguments = [[1, 1], [1], [2, 2], [1], [2]]
Output: [1, -1, 2]
```
With room for one key, `put(2, 2)` evicts key 1 even though key 1 has the higher use count, because it is the only key in the cache.
**Example 3**
```text
Input: capacity = 2,
operations = ["put", "get", "put", "get", "get", "get", "put", "put", "put", "get", "get", "get"],
arguments = [[1, 1], [1], [2, 2], [2], [2], [1], [3, 3], [2, 20], [4, 4], [1], [2], [4]]
Output: [1, 2, 2, 1, 1, -1, 4]
```
Keys 1 and 2 both reach a use count of 3, and key 2 was used less recently, so `put(3, 3)` evicts key 2. `put(2, 20)` evicts key 3, whose count is 1, and key 2 comes back with a count of 1 rather than its old count, so `put(4, 4)` evicts key 2 instead of key 1.
Overview: Build a fixed-capacity key-value cache that evicts the key with the fewest uses, breaking ties by least recent use, and replay a sequence of get and put operations to return each read result. It tests careful per-key frequency and recency tracking with constant average time per operation.
Read the full Alibaba Software Engineer interview experience this question came from
Implement a key-value cache with a fixed capacity. When the cache is full and a new key has to be inserted, it evicts the key that has been used the fewest times. If several keys share the smallest use count, it evicts the one among them that was used least recently.
The cache is tested through one function, `simulate_lfu_cache(capacity, operations, arguments)`. It applies the operations in order to an initially empty cache and returns the results of the read operations. `operations[i]` is `"get"` or `"put"`. For `"get"`, `arguments[i]` is `[key]`; for `"put"`, it is `[key, value]`.
### Rules
- Every key in the cache has a use count.
- `get(key)`: if `key` is in the cache, the result is its value, its use count increases by 1, and it becomes the most recently used key. Otherwise the result is `-1` and the cache does not change.
- `put(key, value)`: if `key` is already in the cache, its value is replaced, its use count increases by 1, and it becomes the most recently used key; nothing is evicted. Otherwise, if the cache already holds `capacity` keys, one key is evicted first: the key with the smallest use count, and among keys tied for the smallest count, the least recently used one. Then `key` is inserted with `value` and a use count of 1, and it becomes the most recently used key.
- An evicted key loses its count. If it is inserted again later, its count starts again at 1.
- Return one integer per `"get"` operation, in the order the operations appear. `"put"` operations produce no output, so a sequence without any `"get"` returns an empty list.
- Each operation must run in O(1) average time.
### Constraints
- `1 <= capacity <= 10^4`
- `1 <= len(operations) <= 2 * 10^5`, and `len(arguments) == len(operations)`
- `0 <= key <= 10^5`
- `0 <= value <= 10^9`
- Every operation is `"get"` or `"put"`, with the matching number of arguments.
Every key, value and result fits in a signed 32-bit integer (none exceeds 2^31 - 1), so `int` is sufficient in Java and C++.
### Example 1
```text
Input: capacity = 3,
operations = ["put", "put", "put", "get", "get", "put", "get", "get", "put", "get", "put", "put", "get", "get", "get"],
arguments = [[1, 100], [2, 200], [3, 300], [2], [3], [4, 400], [1], [4], [5, 500], [2], [3, 333], [6, 600], [5], [3], [6]]
Output: [200, 300, -1, 400, -1, -1, 333, 600]
```
Key 1 is the only key never read, so `put(4, 400)` evicts it. Just before `put(5, 500)`, keys 2, 3 and 4 all have a use count of 2, and key 2 was used least recently, so it is evicted. `put(3, 333)` updates key 3 and raises its count to 3. Just before `put(6, 600)`, key 5 has the smallest count, 1, and is evicted.
### Example 2
```text
Input: capacity = 2,
operations = ["put", "get", "put", "get", "get", "get", "put", "put", "put", "get", "get", "get"],
arguments = [[1, 1], [1], [2, 2], [2], [2], [1], [3, 3], [2, 20], [4, 4], [1], [2], [4]]
Output: [1, 2, 2, 1, 1, -1, 4]
```
Keys 1 and 2 both reach a use count of 3, and key 2 was used less recently, so `put(3, 3)` evicts key 2. `put(2, 20)` evicts key 3, whose count is 1, and key 2 comes back with a count of 1 rather than its old count, so `put(4, 4)` evicts key 2 instead of key 1.
Constraints
- 1 <= capacity <= 10^4
- 1 <= len(operations) <= 2 * 10^5, and len(arguments) == len(operations)
- 0 <= key <= 10^5
- 0 <= value <= 10^9
- Every operation is "get" or "put", with the matching number of arguments.
Examples
Input: (3, ['put', 'put', 'put', 'get', 'get', 'put', 'get', 'get', 'put', 'get', 'put', 'put', 'get', 'get', 'get'], [[1, 100], [2, 200], [3, 300], [2], [3], [4, 400], [1], [4], [5, 500], [2], [3, 333], [6, 600], [5], [3], [6]])
Expected Output: [200, 300, -1, 400, -1, -1, 333, 600]
Explanation: Source Example 1: never-read key 1 is evicted first, then key 2 as the least recently used of three keys tied at count 2, then key 5 at count 1.
Input: (2, ['put', 'get', 'put', 'get', 'get', 'get', 'put', 'put', 'put', 'get', 'get', 'get'], [[1, 1], [1], [2, 2], [2], [2], [1], [3, 3], [2, 20], [4, 4], [1], [2], [4]])
Expected Output: [1, 2, 2, 1, 1, -1, 4]
Explanation: Source Example 3: tie at count 3 evicts the less recently used key 2; key 2 comes back with count 1 and is evicted again by put(4, 4) instead of key 1.
Hints
- When the cache is full you need the key with the smallest use count and, among those, the least recently used one. Think about how to keep the keys organized so that key can be found without scanning the whole cache.
- A successful get and a put on a key that is already present both count as a use: the count goes up by 1 and the key becomes the most recently used. A get on a missing key changes nothing, and a put on a present key never evicts.
- An evicted key forgets its count. If it is put again later, it behaves exactly like a brand-new key with a count of 1.