What you will build
This assignment joins everything in the module into one program. You will write a small lookup service that reads a stream of commands from input and prints answers. The service keeps three kinds of state at once. It keeps an LRU cache of recent prices, so the newest entries stay and the oldest are evicted. It keeps a sorted multiset of all prices added, so it can count prices in a range and find the nearest price. It keeps a set of ids it has already seen, so it can tell a first visit from a repeat.
No single container does all three jobs well. The cache needs a hash map and a linked list. The range and nearest questions need a sorted container. The repeat check needs a hash set. Working out which container fits which job is the real exercise. Read the whole spec before you write any code, and then build the program in the order the hints suggest.
You may not add any library that is outside the C++ standard library. Use g++ -std=c++17, and read the input with cin.
The specification
The first line holds two integers. The first is c, the capacity of the LRU cache, with 1 <= c <= 100. The second is q, the number of commands that follow, with 1 <= q <= 1000.
After that come exactly q lines. Each line starts with one of six command words. The rest of the line holds that command's arguments. Every value is a whole number in the range 0 to 1000000000, except where the command says otherwise.
| Command | Arguments | What it does | What it prints |
|---|---|---|---|
add | p | Adds one price p to the price list. Repeated prices are all kept. | Nothing |
count | lo hi | Counts the prices p with lo <= p <= hi. Always lo <= hi. | The count |
nearest | v | Finds the smallest price that is at least v. | The price, or NONE if no price qualifies |
once | id | Records that the id was seen. The first time an id appears, the answer is new. | NEW the first time, REPEAT after that |
put | id price | Stores a price for an id in the LRU cache. | Nothing |
get | id | Returns the cached price for an id, and counts as a use. | The price, or -1 on a miss |
Three rules matter for the cache.
- A
putfor an id that is already cached updates its price and makes it the most recent entry. It does not evict anything. - A
putfor a new id, when the cache already holdscentries, evicts the least recently used entry first. Then it stores the new one as most recent. - A
getthat finds the id makes it the most recent entry. Agetthat misses prints-1and changes nothing.
The -1 answer is safe because every stored price is between 0 and 1000000000. A real price is never negative, so -1 can only mean a miss.
The price list and the cache are separate. An add puts a price in the list, and that does not put anything in the cache. A put stores a price for an id in the cache, and that does not add the price to the list. The two never affect each other.
The once command has its own state. The set of seen ids starts empty and grows for the whole run. It is not cleared by evictions, and an id that was evicted from the cache still counts as seen.
Worked examples
The first example uses capacity 2 and eleven commands. Read it line by line, and check each answer against the rules above before you run your program.
2
11
add 40
add 15
add 40
count 10 40
nearest 16
nearest 41
once 7
once 7
put 7 99
put 8 15
get 7The expected output is:
3
40
NONE
NEW
REPEAT
99The count is 3, because the prices in the range from 10 to 40 are 15, 40 and 40. The nearest price at or above 16 is 40. Nothing is at or above 41, so the answer is NONE. The first once 7 prints NEW, and the second prints REPEAT. The two put lines fill the cache with ids 7 and 8, and the cache holds two entries. The get 7 finds the price 99, because id 7 was not evicted. The capacity of 2 was just enough to keep it.
The second example uses capacity 1, which makes every new id evict the only entry.
1
6
put 1 5
put 2 6
get 1
get 2
once 1
once 1The expected output is:
-1
6
NEW
REPEATThe put of id 2 evicts id 1, so get 1 misses and prints -1. The id 2 is still cached, so get 2 prints 6. The once lines are unrelated to the cache. Id 1 has never been seen by once, so the first call prints NEW, and the second prints REPEAT. This example checks that you keep the two kinds of memory separate.
Before you move on, write down the answer for one command of your own choosing, and check it against your program. Making your own small example is the fastest way to find a rule you have misread.
Starter code
The program below reads the header and the commands, and it prints nothing yet. It compiles, so you can build on it. Replace each TODO with the logic for that command. Keep reading the arguments for every command, even the ones you have not finished, so the input stays aligned.
To test the starter, type the first example into the Input box. It prints nothing, which is correct for a program that has not been built yet.
Requirements checklist
Tick each box as you finish the item. A program that passes the examples can still miss an item, so check the list before you submit.
- The program reads
candqfrom the first line, and then reads exactlyqcommands. - Every command reads its own arguments, including the commands the program does not yet answer.
-
addstores every price, including repeats, in amultiset. -
count lo hiuseslower_boundandupper_bound, and prints the distance between them. -
nearest vuseslower_boundand printsNONEwhen the result isend(). -
once iduses asetorunordered_set, and printsNEWorREPEATfrom the result ofinsert. - The cache is an
unordered_mapfrom id to an iterator, with alistin usage order. -
puton an existing id updates the price and moves the entry to the front, without evicting. -
puton a new id evicts the back entry when the cache is full, and erases its id from the map first. -
getmoves a hit to the front, and prints-1on a miss without changing the cache. -
addandputprint nothing. - The cache and the price list never affect each other, and evictions do not clear the seen ids.
- Each command costs O(log n) or O(1) average, with no loop over all stored prices or entries.
The last item is the one most often missed. A solution that sorts or scans the whole price list for each nearest command is correct on small inputs and too slow on large ones. Test with a few hundred commands to see whether the program keeps up.
Grading rubric
The rubric describes what each score level looks like in practice. Your program is graded by running it on hidden inputs and comparing the output with the expected output.
| Score | What the program does |
|---|---|
| 60 percent | Compiles and reads the input correctly. add, count and nearest give the right answers on simple inputs without repeats. put and get work when the cache never fills up. |
| 80 percent | All six commands work on the examples. Eviction is correct, including the rule that get counts as a use. once prints NEW and REPEAT correctly. nearest prints NONE when no price qualifies. |
| 100 percent | Everything at 80 percent, plus: updates to an existing id do not evict, duplicate prices are counted correctly, evicted ids still count as seen, capacity 1 works, and the program handles 1000 commands quickly. The code uses no [] lookups that would insert a missing key by accident. |
Most points are lost on three things. The first is eviction order, because a get that does not move the node gives the wrong evictions. The second is the count boundaries, because the upper bound must be inclusive and the bounds are easy to swap. The third is inserting by accident with [], which changes the size of a container and then changes later answers.
Step-by-step hints
Work in this order. Each step builds on the one before it, and each one can be tested on its own. The hints are hidden, so try a step before you open its hint.
Hint 1: build the price commands first
Start with add, count and nearest, and ignore the cache for now. Use a multiset<long long> for the prices. For count, call lower_bound(lo) and upper_bound(hi), and take the distance between them. For nearest, call lower_bound(v) and check whether the result equals end() before you read it. The first checkpoint below does exactly this, and it skips the other commands by reading their arguments.
On the first example this checkpoint prints 3, 40 and NONE, and nothing else.
Hint 2: add the LRU cache
Copy the cache class from lesson 5. It uses long long for keys and values so that it matches the input range. Its put method must handle the update case first, then the eviction, then the insert, in that order. Its get method returns -1 on a miss and splices the node to the front on a hit. The second checkpoint below runs the cache on its own, and it skips the price and seen-id commands.
On the second example this checkpoint prints -1 and 6. On the first example it prints 99.
Hint 3: add the seen ids
Add an unordered_set<long long>. For once id, call insert(id) and read its .second member, which is true when the id was new. Print NEW for true and REPEAT for false. Do not use count followed by insert, because that is two lookups for one answer. The reference solution below shows the single-call form.
Hint 4: check the edge cases
Test the program with these small inputs, and predict each answer before you run it. Capacity 1, with an id that is put twice in a row. A count over a range with no prices. A nearest query above the largest price. A get on an id that was evicted. A put that updates an id which is the least recent entry, followed by a put that should evict something else. Each of these catches a rule that is easy to misread.
Reference solution
Try the whole program yourself before you open this section. The reference is a complete program that answers every command. Its structure is the same as the hints, combined into one class for the cache and one loop for the commands.
Reference solution
The program runs in O(log n) for the price commands and O(1) average for the cache and the seen-id set. The memory is O(c + n + m), where n is the number of prices added and m is the number of distinct ids seen. The cache reads with find every time. It uses [] only in put, to store the iterator of a key it has just inserted, so no read ever creates an entry by accident.
Stretch extension
Once the program passes every example, try one extension. Add a command top k that prints the k largest prices in descending order, with one price per line. Think about which end of the multiset to walk, and how to stop when fewer than k prices exist. Then test a case where the prices contain duplicates, and decide whether duplicates should appear more than once in the output. Explain your choice in a comment at the top of the program.
Final checks before you submit
Run the program on both examples and check each output line. Run it on an input where most commands are add or count, with a few hundred prices, to check speed. Then read through the checklist one more time and make sure every box is ticked for a reason you can explain.
A good submission also includes a short comment above each container. The comment says which command uses it and why that container was the right choice. A reviewer should be able to read the comments and see the design in less than a minute.
What you now know
- A hash map finds keys in O(1) average time, and it does not keep order.
- A sorted multiset counts ranges and finds the nearest value in O(log n), using
lower_boundandupper_bound. - An LRU cache combines a hash map and a linked list, so
getandputboth run in O(1) average time. - A hash set answers the "have I seen this before?" question, and
insertreports whether the value was new. - Keeping separate pieces of state separate, and reading the arguments of every command, are the habits that keep a long program correct.