Facebook Pixel

2353. Design a Food Rating System

MediumDesignArrayHash TableStringOrdered SetHeap (Priority Queue)
Leetcode Link

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 food
  • cuisines[i] - the cuisine type of the i-th food
  • ratings[i] - the initial rating of the i-th food

The FoodRatings class must implement three methods:

  1. Constructor FoodRatings(foods, cuisines, ratings): Initializes the system with the given food information.

  2. changeRating changeRating(food, newRating): Updates the rating of the specified food to newRating.

  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Know which cuisine it belongs to (to find the right sorted list)
  2. Know its current rating (to remove the old entry)
  3. 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: A defaultdict(SortedList) that maps each cuisine to a sorted list of (-rating, food) tuples
  • self.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:

  1. Add (-rating, food) to the sorted list for its cuisine. The negative rating ensures the list is sorted in descending order by rating.
  2. 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:

  1. Retrieve the food's current rating and cuisine from self.g
  2. Update the stored rating in self.g to the new value
  3. Remove the old (-oldRating, food) tuple from the cuisine's sorted list
  4. 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) where n is the number of foods
  • changeRating: O(log m) where m 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 Evaluator

Example 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)

  1. Look up "ramen" in self.g: get (15, "japanese")
  2. Update self.g["ramen"] to (10, "japanese")
  3. Remove (-15, "ramen") from self.d["japanese"]
  4. 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)

  1. Look up "sushi" in self.g: get (8, "japanese")
  2. Update self.g["sushi"] to (12, "japanese")
  3. Remove (-8, "sushi") from self.d["japanese"]
  4. 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) where n is the number of foods. The constructor iterates through all foods once with O(n) iteration, and for each food, it performs an insertion into a SortedList which takes O(log k) time where k is the current size of the SortedList for that cuisine. In the worst case where all foods belong to the same cuisine, this becomes O(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) where k is the number of foods in that cuisine
    • Addition to SortedList: O(log k) where k 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 is O(log n)
  • 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 space
  • self.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))
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More