2353. Design a Food Rating System

MediumDesignHash TableOrdered SetHeap (Priority Queue)
Leetcode Link

Problem Description

The task is to design a food rating system that allows two primary operations:

  1. Modify the rating of a food item that is already in the system.
  2. Retrieve the highest-rated food item for a specific type of cuisine.

To implement this, we need to define a FoodRatings class with the following functionalities:

  • A constructor FoodRatings(String[] foods, String[] cuisines, int[] ratings) that initializes the system with the given food items, their associated cuisines, and their initial ratings. The parameters foods, cuisines, and ratings form a list where each index i corresponds to the ith food's name, type of cuisine, and initial rating.

  • A method changeRating(String food, int newRating) to change the current rating of a specific food item identified by its name.

  • A method highestRated(String cuisine) that returns the name of the food item with the highest rating within a given cuisine. In the case of a tie, the method should return the food item with the lexicographically smaller name.

The class should keep track of the rating updates and always be able to return the correct highest-rated food for any cuisine query.

Intuition

When approaching the design of this food rating system, our goal is to efficiently update ratings and retrieve the highest-rated food items. To achieve this, we make use of the following ideas:

  • Maintain a mapping (mp) that associates each food item's name with its cuisine and current rating. This allows us to quickly access and update the rating of a food item when changeRating is called.

  • Use a data structure (t) where the foods are sorted based on their ratings and names for each cuisine type. The SortedSet from the sortedcontainers library is suitable as it maintains the order of the elements according to a given key. The key is a lambda function that sorts the foods first by their ratings (in descending order) and then by their name (in lexicographical order) in case of a tie in ratings. This ensures that the first element in the SortedSet for each cuisine is always the highest-rated item (with ties broken by name).

  • When changing the rating of a food item, we access its previous rating from the mapping, remove the old rating-food pair from the sorted set corresponding to its cuisine, update the rating in the mapping, and then add the new rating-food pair to the sorted set.

  • For retrieving the highest-rated food for a cuisine, we simply take the first element of the sorted set for the given cuisine as it is already sorted according to our needs.

Using these intuitions, we implement the FoodRatings class to enable quick rating updates and retrieval of the top-rated food items per cuisine type.

Learn more about Heap (Priority Queue) patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Solution Approach

In the given solution, we have two critical operations to implement efficiently for the FoodRatings class: updating food ratings and retrieving the highest-rated food for a given cuisine. Let's explore how the solution approach uses algorithms and data structures to fulfill these operations:

  1. Data Structure Initialization:

    • In the constructor, we initialize two data structures mp and t. The mp (a Python dictionary) stores the relationship between food items and their properties (cuisine type and rating).
    • Additionally, we have a defaultdict called t, which is a dictionary that automatically creates a SortedSet when a new key is accessed. Each key in t is a cuisine, and each value is a SortedSet that holds tuples of (rating, food_name).
  2. Adding Food Items:

    • We loop over the input lists of foods, cuisines, and ratings while initializing. For each food item, we update mp and t.
    • mp gets the entry mp[food_name] = (cuisine, rating), which maps food names to their respective cuisines and ratings.
    • t gets the entry t[cuisine].add((rating, food_name)), which automatically orders the food items into the correct position in the SortedSet. The key function ensures that the items are ordered first by rating value in descending order (hence the negative sign) and then by food name lexicographically.
  3. Updating Food Ratings:

    • In the changeRating method, we first retrieve the current cuisine and rating from mp using the food's name.
    • We then update the mp entry with the new rating.
    • Next, we need to update the SortedSet in t. Since SortedSet does not have an efficient way to update an element, we first remove the old rating-food tuple and then add a new tuple with the updated rating.
    • This keeps our SortedSet in t orderly, ensuring that the highest-rated food is at index 0.
  4. Retrieving the Highest-Rated Food:

    • The highestRated method is straightforward because of our sorted data in t.
    • We simply return the first food item in the SortedSet for the passed cuisine, which is guaranteed to be the highest-rated due to our key function used in the SortedSet. The tuple at index 0 is the one with the highest rating, and in the event of a rating tie, the lexicographically smaller name.

By using a combination of a dictionary to maintain food properties and sorted sets to organize the food items within cuisines, we get a system that can perform updates and retrievals in an efficient manner. The choice of SortedSet is particularly poignant because it maintains the desired order of elements at all times, abstracting away the need for manual sorting after each update.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which two pointer techniques do you use to check if a string is a palindrome?

Example Walkthrough

Let's consider a small example to illustrate the solution approach described.

Suppose we have the following initial data:

  • Foods: ["apple pie", "burger", "chicken sandwich"]
  • Cuisines: ["American", "American", "American"]
  • Ratings: [5, 3, 4]

We create an instance of the FoodRatings class by making a call to its constructor:

1fr = FoodRatings(["apple pie", "burger", "chicken sandwich"], ["American", "American", "American"], [5, 3, 4])

During initialization:

  1. The constructor sets up the mapping mp with keys as food names and values as a tuple of cuisine and rating. For example, mp["apple pie"] = ("American", 5).

  2. It also initializes a defaultdict of SortedSets called t. As there's only one cuisine "American" all food items are added to t["American"]:

    • t["American"] = {(-5, "apple pie"), (-3, "burger"), (-4, "chicken sandwich")}

Now, let's use the changeRating method to update the rating of "burger" to 5:

1fr.changeRating("burger", 5)

Here's what happens internally:

  1. The mp entry for "burger" gets updated to mp["burger"] = ("American", 5).

  2. In t, we remove the old entry (-3, "burger") and add a new entry (-5, "burger") because the ratings are stored with a negative sign for descending order.

  3. After the update, t["American"] is now: t["American"] = {(-5, "apple pie"), (-5, "burger"), (-4, "chicken sandwich")}. Notice "apple pie" and "burger" have the same rating, but "apple pie" comes first because it's lexicographically smaller.

When we call highestRated method to get the highest-rated American cuisine food:

1highest_rated_american = fr.highestRated("American")

We will get "apple pie" as the output, as it is the first element of the SortedSet t["American"], which tells us it has the highest rating, and since it's tied with "burger", it comes first due to lexicographical order.

Thus, the class successfully handles updates to ratings and retrieves the highest-rated item for each cuisine type efficiently, as demonstrated in this small example.

Solution Implementation

1from sortedcontainers import SortedSet
2from collections import defaultdict
3
4class FoodRatings:
5    def __init__(self, foods, cuisines, ratings):
6        # Initialize two dictionaries to store our food ratings and sorted sets
7        # Maps each food to its cuisine and rating
8        self.food_to_cuisine_and_rating = {}
9      
10        # Maps each cuisine to a sorted set of tuples with ratings and corresponding food names
11        self.cuisine_to_sorted_foods = defaultdict(lambda: SortedSet(key=lambda x: (-x[0], x[1])))
12
13        # Populate the dictionaries with the initial list of foods, cuisines, and ratings
14        for food, cuisine, rating in zip(foods, cuisines, ratings):
15            self.food_to_cuisine_and_rating[food] = (cuisine, rating)
16            self.cuisine_to_sorted_foods[cuisine].add((rating, food))
17
18    def change_rating(self, food, new_rating):
19        """
20        Update the rating of the specified food.
21        """
22        # Retrieve the current cuisine and rating for the food
23        cuisine, current_rating = self.food_to_cuisine_and_rating[food]
24      
25        # Update the food's rating in the mapping
26        self.food_to_cuisine_and_rating[food] = (cuisine, new_rating)
27      
28        # Remove the old rating from the sorted set and add the new rating
29        self.cuisine_to_sorted_foods[cuisine].remove((current_rating, food))
30        self.cuisine_to_sorted_foods[cuisine].add((new_rating, food))
31
32    def highest_rated(self, cuisine):
33        """
34        Return the name of the food that has the highest rating for the given cuisine.
35        """
36        # The first element of the SortedSet for the cuisine has the highest rating due to sorting order
37        return self.cuisine_to_sorted_foods[cuisine][0][1]
38      
39# Note: While the method names are not modified as per instructions (changeRating, highestRated), 
40# the variable naming has been standardized to adhere to Python's snake_case convention.
41
1import java.util.*;
2
3// Class to manage food ratings based on their cuisine type.
4public class FoodRatings {
5
6    // Map to store the rating and cuisine for each food item.
7    private Map<String, Pair<Integer, String>> foodMap;
8    // Map to store trees of food items by cuisine, sorted by descending ratings.
9    private Map<String, TreeSet<Pair<Integer, String>>> cuisineMap;
10
11    // Convenience class for pairs, since Java doesn't have a native pair structure.
12    private static class Pair<T1, T2> implements Comparable<Pair<T1, T2>> {
13        T1 first;
14        T2 second;
15
16        Pair(T1 first, T2 second) {
17            this.first = first;
18            this.second = second;
19        }
20
21        // Implementing the compareTo method for sorting
22        public int compareTo(Pair<T1, T2> o) {
23            // Assuming first is Comparable, because we only use Integer in this context
24            int cmp = -((Comparable) first).compareTo(o.first);
25            if (cmp != 0) {
26                return cmp;
27            }
28            // Assuming second is Comparable (because it's a String in our usage)
29            return ((Comparable) second).compareTo(o.second);
30        }
31    }
32
33    // Constructor that initializes the FoodRatings object with the given data.
34    public FoodRatings(List<String> foods, List<String> cuisines, List<Integer> ratings) {
35        foodMap = new HashMap<>();
36        cuisineMap = new HashMap<>();
37
38        // Initialize data structure with the foods, ratings, and cuisines provided.
39        for (int i = 0; i < foods.size(); i++) {
40            String food = foods.get(i);
41            String cuisine = cuisines.get(i);
42            int rating = ratings.get(i);
43          
44            foodMap.put(food, new Pair<>(rating, cuisine));
45
46            // Insert into TreeSet with reversed rating for natural ordering.
47            cuisineMap.computeIfAbsent(cuisine, k -> new TreeSet<>())
48                    .add(new Pair<>(-rating, food));
49        }
50    }
51
52    // Updates the rating of a food item.
53    public void changeRating(String food, int newRating) {
54        Pair<Integer, String> currentPair = foodMap.get(food);
55      
56        // Remove the (rating, food) pair from the tree set of the particular cuisine.
57        cuisineMap.get(currentPair.second).remove(new Pair<>(-currentPair.first, food));
58      
59        // Update the food's rating in the food map.
60        currentPair.first = newRating;
61      
62        // Insert the updated (rating, food) pair back into the tree set.
63        cuisineMap.get(currentPair.second).add(new Pair<>(-newRating, food));
64    }
65
66    // Returns the food item with the highest rating within a specified cuisine.
67    public String highestRated(String cuisine) {
68        // The first element in the TreeSet has the highest rating due to our ordering.
69        return cuisineMap.get(cuisine).first().second;
70    }
71}
72
73// Driver code example
74/*
75public static void main(String[] args) {
76    List<String> foods = Arrays.asList("ramen", "tacos", "burger");
77    List<String> cuisines = Arrays.asList("japanese", "mexican", "american");
78    List<Integer> ratings = Arrays.asList(10, 9, 5);
79
80    FoodRatings foodRatings = new FoodRatings(foods, cuisines, ratings);
81
82    System.out.println(foodRatings.highestRated("japanese")); // Should output "ramen"
83
84    foodRatings.changeRating("ramen", 12);
85
86    System.out.println(foodRatings.highestRated("japanese")); // Should output "ramen" with updated rating
87}
88*/
89
1#include <iostream>
2#include <string>
3#include <vector>
4#include <map>
5#include <set>
6using namespace std;
7
8// Define a convenience type for (rating, food) pairs, used to sort foods by ratings.
9using RatingFoodPair = pair<int, string>;
10
11class FoodRatings {
12    // Map to store the rating and cuisine for each food item.
13    map<string, RatingFoodPair> foodMap;
14    // Map to store sets of foods by cuisine where each set is sorted by rating.
15    map<string, set<RatingFoodPair>> cuisineMap;
16
17public:
18    // Constructor that initializes the FoodRatings object with the given food items,
19    // their respective cuisines, and their initial ratings.
20    FoodRatings(vector<string>& foods, vector<string>& cuisines, vector<int>& ratings) {
21        int numFoods = foods.size();
22        for (int i = 0; i < numFoods; ++i) {
23            string food = foods[i], cuisine = cuisines[i];
24            int rating = ratings[i];
25
26            // The minus sign is used to sort the ratings in descending order by default,
27            // as the set is ordered in ascending order.
28            foodMap[food] = RatingFoodPair(rating, cuisine);
29            cuisineMap[cuisine].insert(RatingFoodPair(-rating, food));
30        }
31    }
32
33    // Updates the rating of the specified food item.
34    void changeRating(string food, int newRating) {
35        // Get the current rating and cuisine for the food.
36        RatingFoodPair& currentPair = foodMap[food];
37      
38        // Remove the current (rating, food) pair from the cuisine set.
39        cuisineMap[currentPair.second].erase(RatingFoodPair(-currentPair.first, food));
40      
41        // Update the food's rating in the food map.
42        currentPair.first = newRating;
43
44        // Insert the updated (newRating, food) pair into the cuisine set.
45        cuisineMap[currentPair.second].insert(RatingFoodPair(-newRating, food));
46    }
47
48    // Returns the name of the food item with the highest rating for the specified cuisine.
49    string highestRated(string cuisine) {
50        // The foods are sorted in descending order by rating in the set.
51        // *cuisineMap[cuisine].begin() gives the first pair, which has the highest rating.
52        return cuisineMap[cuisine].begin()->second;
53    }
54};
55
56/**
57 * Your FoodRatings object will be instantiated and called as such:
58 * FoodRatings* obj = new FoodRatings(foods, cuisines, ratings);
59 * obj->changeRating(food, newRating);
60 * string result = obj->highestRated(cuisine);
61 */
62
63// Examples:
64
65/*
66int main() {
67    vector<string> foods = {"ramen", "tacos", "burger"};
68    vector<string> cuisines = {"japanese", "mexican", "american"};
69    vector<int> ratings = {10, 9, 5};
70
71    FoodRatings* foodRatings = new FoodRatings(foods, cuisines, ratings);
72
73    cout << foodRatings->highestRated("japanese") << endl; // Should output "ramen"
74
75    foodRatings->changeRating("ramen", 12);
76
77    cout << foodRatings->highestRated("japanese") << endl; // Should output "ramen" with updated rating
78
79    delete foodRatings;
80    return 0;
81}
82*/
83
1// Import necessary libraries
2import { Set } from 'typescript-collections';
3
4// Define a convenience type for (rating, food) pairs, used to sort foods by ratings.
5type RatingFoodPair = [number, string];
6
7// Map to store the rating and cuisine for each food item.
8const foodMap: Record<string, RatingFoodPair> = {};
9
10// Map to store sets of foods by cuisine where each set is sorted by rating.
11const cuisineMap: Record<string, Set<RatingFoodPair>> = {};
12
13// Initializes the FoodRatings object with the given food items, their respective cuisines, and their initial ratings.
14function initializeFoodRatings(foods: string[], cuisines: string[], ratings: number[]): void {
15    for (let i = 0; i < foods.length; ++i) {
16        const food = foods[i];
17        const cuisine = cuisines[i];
18        const rating = ratings[i];
19
20        foodMap[food] = [rating, cuisine];
21        if (!cuisineMap[cuisine]) {
22            // Initialize a set with a custom comparator that compares RatingFoodPair
23            cuisineMap[cuisine] = new Set<RatingFoodPair>((a, b) => {
24                if (a[0] === b[0]) return a[1].localeCompare(b[1]);
25                return b[0] - a[0];
26            });
27        }
28        cuisineMap[cuisine].add([-rating, food]); // Using negative rating for descending order
29    }
30}
31
32// Updates the rating of the specified food item.
33function changeRating(food: string, newRating: number): void {
34    const [currentRating, cuisine] = foodMap[food];
35    cuisineMap[cuisine].remove([-currentRating, food]); // Remove the current (rating, food) pair from the set
36    foodMap[food] = [newRating, cuisine]; // Update the food's rating in the food map
37    cuisineMap[cuisine].add([-newRating, food]); // Add the new (rating, food) pair to the set
38}
39
40// Returns the name of the food item with the highest rating for the specified cuisine.
41function highestRated(cuisine: string): string {
42    const sortedFoods = cuisineMap[cuisine].toArray(); // Convert set to array
43    return sortedFoods[0][1]; // Select the food with the highest rating (first item in the sorted array)
44}
45
46// Example usage:
47
48// Initialize food ratings 
49initializeFoodRatings(["ramen", "tacos", "burger"], ["japanese", "mexican", "american"], [10, 9, 5]);
50
51// Output highest-rated Japanese food
52console.log(highestRated("japanese")); // Should output "ramen"
53
54// Change the rating of "ramen"
55changeRating("ramen", 12);
56
57// Output highest-rated Japanese food after rating change
58console.log(highestRated("japanese")); // Should output "ramen" with updated rating
59
Not Sure What to Study? Take the 2-min Quiz:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Time and Space Complexity

Time Complexity

  • __init__ method:

    • Iterating over foods, cuisines, and ratings lists of size n will take O(n) time.
    • Inserting into the sorted set for each food item takes O(log n) time, so overall it would be O(n log n).
  • changeRating method:

    • Accessing the tuple in mp and removing from the sorted set takes O(log n) time.
    • Inserting the new rating into the sorted set again takes O(log n) time.
    • Therefore, each call to changeRating is O(log n).
  • highestRated method:

    • Getting the highest rated food is O(1) since it's just getting the first element of a sorted set.

Overall, init will dominate the time complexity with O(n log n), and subsequent calls to changeRating will be O(log n), while highestRated calls will be O(1).

Space Complexity

  • self.mp dictionary will store n entries where n is the total number of foods, so it takes O(n) space.
  • self.t will also store n entries in various SortedSets, so it also takes O(n) space.

Considering both mp and t, the total space complexity will also be O(n), keeping single entries for each food item in the system.

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

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


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫