2353. Design a Food Rating System
Problem Description
You need to design a food rating system that tracks foods, their cuisines, and ratings. The system should support two main operations: modifying food ratings and finding the highest-rated food for a specific cuisine.
The system is initialized with three arrays of equal length n
:
foods[i]
- the name of the i-th foodcuisines[i]
- the cuisine type of the i-th foodratings[i]
- the initial rating of the i-th food
The FoodRatings
class must implement three methods:
-
Constructor
FoodRatings(foods, cuisines, ratings)
: Initializes the system with the given food information. -
changeRating
changeRating(food, newRating)
: Updates the rating of the specified food tonewRating
. -
highestRated
highestRated(cuisine)
: Returns the name of the highest-rated food for the given cuisine. If multiple foods have the same highest rating, return the one that comes first lexicographically (alphabetically).
For example, if you have foods "pizza" and "pasta" both with rating 10 for Italian cuisine, highestRated("Italian")
should return "pasta" since it comes before "pizza" alphabetically.
The solution uses two data structures:
- A hash table
d
that maps each cuisine to a sorted list of (negative rating, food name) tuples. The negative rating is used to maintain descending order by rating. - A hash table
g
that maps each food to its (rating, cuisine) pair for quick lookups during rating updates.
When changing a rating, the system removes the old (rating, food) entry from the cuisine's sorted list and adds the new one. Finding the highest-rated food simply returns the first element in the cuisine's sorted list.
Intuition
The key insight is recognizing that we need to efficiently handle two types of queries: updating ratings and finding the highest-rated food per cuisine. This suggests we need fast lookups and ordered data structures.
When thinking about finding the highest-rated food for a cuisine, we need to maintain foods sorted by rating. But since ratings can change, we need a data structure that allows both insertion and deletion while maintaining order - this points us toward using a sorted list or ordered set.
The challenge is that when we update a food's rating, we need to:
- Know which cuisine it belongs to (to find the right sorted list)
- Know its current rating (to remove the old entry)
- Insert the new rating in the correct position
This leads to the idea of maintaining two hash tables:
- One to group foods by cuisine with their ratings in sorted order
- Another to quickly look up a food's current rating and cuisine
For the sorted storage, we use (negative rating, food name)
tuples because:
- Python's
SortedList
sorts in ascending order by default - We want highest ratings first, so negating the rating achieves descending order
- When ratings are equal, the tuple naturally sorts by the second element (food name) lexicographically
The beauty of this approach is that highestRated
becomes a simple O(1) operation - just return the first element in the sorted list for that cuisine. The changeRating
operation involves removing one element and adding another to a sorted list, which are both O(log n) operations in a balanced tree structure.
This design trades some space (storing data in two structures) for time efficiency, ensuring all operations are fast even with frequent rating updates.
Learn more about Heap (Priority Queue) patterns.
Solution Approach
The implementation uses two hash tables and a sorted list data structure to efficiently manage the food rating system.
Data Structures:
self.d
: Adefaultdict(SortedList)
that maps each cuisine to a sorted list of(-rating, food)
tuplesself.g
: A dictionary that maps each food to its(rating, cuisine)
pair
Constructor Implementation:
The constructor iterates through the input arrays and populates both data structures:
for food, cuisine, rating in zip(foods, cuisines, ratings):
self.d[cuisine].add((-rating, food)) # Add to cuisine's sorted list
self.g[food] = (rating, cuisine) # Store food's info
For each food, we:
- Add
(-rating, food)
to the sorted list for its cuisine. The negative rating ensures the list is sorted in descending order by rating. - Store the food's rating and cuisine in
self.g
for quick lookups.
changeRating Method:
When updating a food's rating:
def changeRating(self, food: str, newRating: int) -> None:
oldRating, cuisine = self.g[food] # Get current info
self.g[food] = (newRating, cuisine) # Update stored rating
self.d[cuisine].remove((-oldRating, food)) # Remove old entry
self.d[cuisine].add((-newRating, food)) # Add new entry
The process involves:
- Retrieve the food's current rating and cuisine from
self.g
- Update the stored rating in
self.g
to the new value - Remove the old
(-oldRating, food)
tuple from the cuisine's sorted list - Insert the new
(-newRating, food)
tuple into the sorted list
highestRated Method:
Finding the highest-rated food is straightforward:
def highestRated(self, cuisine: str) -> str:
return self.d[cuisine][0][1] # Return food name from first tuple
Since the sorted list maintains foods in descending order by rating (and ascending lexicographically for ties), the first element [0]
is always the highest-rated food. We return the food name [1]
from the tuple.
Time Complexity:
- Constructor:
O(n log n)
wheren
is the number of foods - changeRating:
O(log m)
wherem
is the number of foods in the cuisine - highestRated:
O(1)
Space Complexity: O(n)
to store all food information in both data structures
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate how the solution works.
Initial Setup:
foods = ["kimchi", "miso", "sushi", "ramen"] cuisines = ["korean", "japanese", "japanese", "japanese"] ratings = [9, 12, 8, 15]
Step 1: Constructor Initialization
When we create the FoodRatings object, two data structures are built:
self.d
(cuisine → sorted list):
- "korean": [(-9, "kimchi")]
- "japanese": [(-15, "ramen"), (-12, "miso"), (-8, "sushi")]
Note how the Japanese foods are sorted by negative rating, so -15 comes first (highest rating 15).
self.g
(food → info):
- "kimchi": (9, "korean")
- "miso": (12, "japanese")
- "sushi": (8, "japanese")
- "ramen": (15, "japanese")
Step 2: Query highestRated("japanese")
Looking at self.d["japanese"]
, the first element is (-15, "ramen")
.
Return: "ramen" (the food with rating 15)
Step 3: changeRating("ramen", 10)
- Look up "ramen" in
self.g
: get (15, "japanese") - Update
self.g["ramen"]
to (10, "japanese") - Remove (-15, "ramen") from
self.d["japanese"]
- Add (-10, "ramen") to
self.d["japanese"]
After this operation:
self.d["japanese"]
: [(-12, "miso"), (-10, "ramen"), (-8, "sushi")]
Step 4: Query highestRated("japanese")
Now the first element is (-12, "miso")
.
Return: "miso" (rating 12 is now highest)
Step 5: changeRating("sushi", 12)
- Look up "sushi" in
self.g
: get (8, "japanese") - Update
self.g["sushi"]
to (12, "japanese") - Remove (-8, "sushi") from
self.d["japanese"]
- Add (-12, "sushi") to
self.d["japanese"]
After this operation:
self.d["japanese"]
: [(-12, "miso"), (-12, "sushi"), (-10, "ramen")]
Note that both "miso" and "sushi" have rating 12, but "miso" comes first lexicographically.
Step 6: Query highestRated("japanese")
The first element is still (-12, "miso")
.
Return: "miso" (when tied at rating 12, "miso" < "sushi" alphabetically)
This example demonstrates how:
- The negative ratings maintain descending order
- Ties are broken lexicographically
- Updates efficiently remove and reinsert elements
- Queries return results in O(1) time
Solution Implementation
1from collections import defaultdict
2from sortedcontainers import SortedList
3from typing import List
4
5class FoodRatings:
6 def __init__(self, foods: List[str], cuisines: List[str], ratings: List[int]):
7 # Dictionary mapping cuisine to a sorted list of (negative rating, food name) tuples
8 # Using negative rating to sort in descending order by rating, ascending by name
9 self.cuisine_to_foods = defaultdict(SortedList)
10
11 # Dictionary mapping food name to (rating, cuisine) tuple
12 self.food_info = {}
13
14 # Initialize data structures with the input arrays
15 for food, cuisine, rating in zip(foods, cuisines, ratings):
16 # Add to sorted list with negative rating for descending order
17 self.cuisine_to_foods[cuisine].add((-rating, food))
18 # Store food information for quick lookup
19 self.food_info[food] = (rating, cuisine)
20
21 def changeRating(self, food: str, newRating: int) -> None:
22 # Retrieve the old rating and cuisine for this food
23 old_rating, cuisine = self.food_info[food]
24
25 # Update the food info with new rating
26 self.food_info[food] = (newRating, cuisine)
27
28 # Remove the old entry from the sorted list
29 self.cuisine_to_foods[cuisine].remove((-old_rating, food))
30
31 # Add the new entry with updated rating to the sorted list
32 self.cuisine_to_foods[cuisine].add((-newRating, food))
33
34 def highestRated(self, cuisine: str) -> str:
35 # Return the food name of the highest rated item for this cuisine
36 # The first element [0] has the highest rating (most negative value)
37 # Index [1] gets the food name from the tuple
38 return self.cuisine_to_foods[cuisine][0][1]
39
40
41# Your FoodRatings object will be instantiated and called as such:
42# obj = FoodRatings(foods, cuisines, ratings)
43# obj.changeRating(food, newRating)
44# param_2 = obj.highestRated(cuisine)
45
1class FoodRatings {
2 // Map from cuisine name to a sorted set of (rating, food) pairs
3 private Map<String, TreeSet<Pair<Integer, String>>> cuisineToFoodSet = new HashMap<>();
4
5 // Map from food name to (rating, cuisine) pair for quick lookup
6 private Map<String, Pair<Integer, String>> foodInfo = new HashMap<>();
7
8 // Comparator for sorting foods by rating (descending) and name (ascending)
9 private final Comparator<Pair<Integer, String>> foodComparator = (a, b) -> {
10 // First compare by rating in descending order
11 if (!a.getKey().equals(b.getKey())) {
12 return b.getKey().compareTo(a.getKey());
13 }
14 // If ratings are equal, compare by food name in ascending order
15 return a.getValue().compareTo(b.getValue());
16 };
17
18 /**
19 * Initialize the food rating system with foods, their cuisines, and initial ratings
20 * @param foods Array of food names
21 * @param cuisines Array of cuisine types corresponding to each food
22 * @param ratings Array of initial ratings corresponding to each food
23 */
24 public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
25 for (int i = 0; i < foods.length; i++) {
26 String food = foods[i];
27 String cuisine = cuisines[i];
28 int rating = ratings[i];
29
30 // Add food to the cuisine's sorted set
31 cuisineToFoodSet.computeIfAbsent(cuisine, k -> new TreeSet<>(foodComparator))
32 .add(new Pair<>(rating, food));
33
34 // Store food information for quick lookup
35 foodInfo.put(food, new Pair<>(rating, cuisine));
36 }
37 }
38
39 /**
40 * Update the rating of a specific food
41 * @param food The name of the food to update
42 * @param newRating The new rating value
43 */
44 public void changeRating(String food, int newRating) {
45 // Get current food information
46 Pair<Integer, String> currentInfo = foodInfo.get(food);
47 int oldRating = currentInfo.getKey();
48 String cuisine = currentInfo.getValue();
49
50 // Update food information map with new rating
51 foodInfo.put(food, new Pair<>(newRating, cuisine));
52
53 // Remove old entry from the cuisine's sorted set
54 cuisineToFoodSet.get(cuisine).remove(new Pair<>(oldRating, food));
55
56 // Add new entry with updated rating to the cuisine's sorted set
57 cuisineToFoodSet.get(cuisine).add(new Pair<>(newRating, food));
58 }
59
60 /**
61 * Get the highest-rated food for a specific cuisine
62 * @param cuisine The cuisine type to query
63 * @return The name of the highest-rated food in the specified cuisine
64 */
65 public String highestRated(String cuisine) {
66 // Return the food name from the first element (highest rated) in the sorted set
67 return cuisineToFoodSet.get(cuisine).first().getValue();
68 }
69}
70
71/**
72 * Your FoodRatings object will be instantiated and called as such:
73 * FoodRatings obj = new FoodRatings(foods, cuisines, ratings);
74 * obj.changeRating(food,newRating);
75 * String param_2 = obj.highestRated(cuisine);
76 */
77
1class FoodRatings {
2public:
3 FoodRatings(vector<string>& foods, vector<string>& cuisines, vector<int>& ratings) {
4 // Initialize the food rating system with the given arrays
5 for (int i = 0; i < foods.size(); ++i) {
6 string food = foods[i];
7 string cuisine = cuisines[i];
8 int rating = ratings[i];
9
10 // Store food-rating pairs for each cuisine in a sorted set
11 // Use negative rating for descending order, food name for lexicographic tiebreaker
12 cuisineToFoodSet[cuisine].insert({-rating, food});
13
14 // Store the rating and cuisine for each food for quick lookup
15 foodInfo[food] = {rating, cuisine};
16 }
17 }
18
19 void changeRating(string food, int newRating) {
20 // Retrieve the current rating and cuisine of the food
21 auto [oldRating, cuisine] = foodInfo[food];
22
23 // Update the food's rating in the lookup map
24 foodInfo[food] = {newRating, cuisine};
25
26 // Remove the old entry from the cuisine's sorted set
27 cuisineToFoodSet[cuisine].erase({-oldRating, food});
28
29 // Insert the new entry with updated rating
30 cuisineToFoodSet[cuisine].insert({-newRating, food});
31 }
32
33 string highestRated(string cuisine) {
34 // Return the food with highest rating for the given cuisine
35 // The set is sorted by rating (descending) and food name (ascending)
36 return cuisineToFoodSet[cuisine].begin()->second;
37 }
38
39private:
40 // Map from cuisine to a set of (negative rating, food name) pairs
41 // Set automatically maintains sorted order: highest rating first, then lexicographically
42 unordered_map<string, set<pair<int, string>>> cuisineToFoodSet;
43
44 // Map from food name to its (rating, cuisine) information
45 unordered_map<string, pair<int, string>> foodInfo;
46};
47
48/**
49 * Your FoodRatings object will be instantiated and called as such:
50 * FoodRatings* obj = new FoodRatings(foods, cuisines, ratings);
51 * obj->changeRating(food,newRating);
52 * string param_2 = obj->highestRated(cuisine);
53 */
54
1// Map from cuisine to a set of [negative rating, food name] pairs
2// Set maintains sorted order: highest rating first (via negative values), then lexicographically
3const cuisineToFoodSet: Map<string, Array<[number, string]>> = new Map();
4
5// Map from food name to its [rating, cuisine] information
6const foodInfo: Map<string, [number, string]> = new Map();
7
8/**
9 * Initialize the food rating system with the given arrays
10 * @param foods - Array of food names
11 * @param cuisines - Array of cuisine types corresponding to each food
12 * @param ratings - Array of ratings corresponding to each food
13 */
14function FoodRatings(foods: string[], cuisines: string[], ratings: number[]): void {
15 // Clear any existing data
16 cuisineToFoodSet.clear();
17 foodInfo.clear();
18
19 // Process each food item
20 for (let i = 0; i < foods.length; i++) {
21 const food = foods[i];
22 const cuisine = cuisines[i];
23 const rating = ratings[i];
24
25 // Initialize cuisine array if it doesn't exist
26 if (!cuisineToFoodSet.has(cuisine)) {
27 cuisineToFoodSet.set(cuisine, []);
28 }
29
30 // Add food-rating pair to cuisine's sorted array
31 // Use negative rating for descending order, food name for lexicographic tiebreaker
32 const cuisineArray = cuisineToFoodSet.get(cuisine)!;
33 cuisineArray.push([-rating, food]);
34
35 // Store the rating and cuisine for each food for quick lookup
36 foodInfo.set(food, [rating, cuisine]);
37 }
38
39 // Sort each cuisine's food array by rating (descending) and food name (ascending)
40 for (const [cuisine, foodArray] of cuisineToFoodSet.entries()) {
41 foodArray.sort((a, b) => {
42 // First compare by rating (negative values for descending order)
43 if (a[0] !== b[0]) {
44 return a[0] - b[0];
45 }
46 // If ratings are equal, compare by food name lexicographically
47 return a[1].localeCompare(b[1]);
48 });
49 }
50}
51
52/**
53 * Update the rating of a specific food
54 * @param food - The name of the food to update
55 * @param newRating - The new rating value
56 */
57function changeRating(food: string, newRating: number): void {
58 // Retrieve the current rating and cuisine of the food
59 const [oldRating, cuisine] = foodInfo.get(food)!;
60
61 // Update the food's rating in the lookup map
62 foodInfo.set(food, [newRating, cuisine]);
63
64 // Get the cuisine's food array
65 const cuisineArray = cuisineToFoodSet.get(cuisine)!;
66
67 // Find and remove the old entry from the cuisine's sorted array
68 const oldIndex = cuisineArray.findIndex(
69 item => item[0] === -oldRating && item[1] === food
70 );
71 if (oldIndex !== -1) {
72 cuisineArray.splice(oldIndex, 1);
73 }
74
75 // Insert the new entry with updated rating
76 cuisineArray.push([-newRating, food]);
77
78 // Re-sort the array to maintain order
79 cuisineArray.sort((a, b) => {
80 // First compare by rating (negative values for descending order)
81 if (a[0] !== b[0]) {
82 return a[0] - b[0];
83 }
84 // If ratings are equal, compare by food name lexicographically
85 return a[1].localeCompare(b[1]);
86 });
87}
88
89/**
90 * Get the highest rated food for a given cuisine
91 * @param cuisine - The cuisine type to query
92 * @returns The name of the highest rated food in the cuisine
93 */
94function highestRated(cuisine: string): string {
95 // Return the food with highest rating for the given cuisine
96 // The array is sorted by rating (descending) and food name (ascending)
97 return cuisineToFoodSet.get(cuisine)![0][1];
98}
99
Time and Space Complexity
Time Complexity:
-
Constructor (
__init__
):O(n log n)
wheren
is the number of foods. The constructor iterates through all foods once withO(n)
iteration, and for each food, it performs an insertion into a SortedList which takesO(log k)
time wherek
is the current size of the SortedList for that cuisine. In the worst case where all foods belong to the same cuisine, this becomesO(n log n)
. -
changeRating
method:O(log n)
in the worst case. This method performs:- Dictionary lookup in
self.g
:O(1)
- Dictionary update in
self.g
:O(1)
- Removal from SortedList:
O(log k)
wherek
is the number of foods in that cuisine - Addition to SortedList:
O(log k)
wherek
is the number of foods in that cuisine - In the worst case where all foods belong to the same cuisine,
k = n
, so the complexity isO(log n)
- Dictionary lookup in
-
highestRated
method:O(1)
. This method simply accesses the first element of the SortedList for the given cuisine, which is a constant-time operation.
Space Complexity: O(n)
where n
is the number of foods.
self.d
stores all foods organized by cuisine in SortedLists:O(n)
total spaceself.g
stores a tuple(rating, cuisine)
for each food:O(n)
space- Total space complexity:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle Lexicographical Ordering for Ties
One of the most common mistakes is not properly handling the case where multiple foods have the same rating. The problem requires returning the lexicographically smallest food name when there's a tie.
Incorrect Approach:
# Wrong: Only storing rating without considering food name for ties self.cuisine_to_foods[cuisine].add((-rating,)) # Missing food name
Correct Solution:
# Store both rating and food name as a tuple self.cuisine_to_foods[cuisine].add((-rating, food))
The SortedList will automatically sort tuples first by the negative rating, then by food name alphabetically when ratings are equal.
2. Using Positive Ratings Instead of Negative
Another frequent error is storing positive ratings in the sorted list, which would sort in ascending order (lowest ratings first) rather than the required descending order.
Incorrect Approach:
# Wrong: This sorts in ascending order by rating self.cuisine_to_foods[cuisine].add((rating, food)) # highestRated would incorrectly return the lowest-rated food
Correct Solution:
# Use negative rating to achieve descending order self.cuisine_to_foods[cuisine].add((-rating, food))
3. Not Updating Both Data Structures in changeRating
A critical mistake is forgetting to update the food_info
dictionary when changing a rating, which would cause future rating changes to use outdated information.
Incorrect Approach:
def changeRating(self, food: str, newRating: int) -> None:
old_rating, cuisine = self.food_info[food]
# Forgot to update food_info!
self.cuisine_to_foods[cuisine].remove((-old_rating, food))
self.cuisine_to_foods[cuisine].add((-newRating, food))
Correct Solution:
def changeRating(self, food: str, newRating: int) -> None:
old_rating, cuisine = self.food_info[food]
# Must update food_info with new rating
self.food_info[food] = (newRating, cuisine)
self.cuisine_to_foods[cuisine].remove((-old_rating, food))
self.cuisine_to_foods[cuisine].add((-newRating, food))
4. Attempting to Remove Non-existent Entries
When implementing changeRating, ensure you're removing the exact tuple that exists in the sorted list. Any mismatch in the tuple structure will cause a ValueError.
Incorrect Approach:
# Wrong: Trying to remove with positive rating when stored as negative self.cuisine_to_foods[cuisine].remove((old_rating, food)) # ValueError!
Correct Solution:
# Remove with the same format used when adding self.cuisine_to_foods[cuisine].remove((-old_rating, food))
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!