Design a food rating system that can do the following:
Implement the FoodRatings class:
FoodRatings(String[] foods, String[] cuisines, int[] ratings) Initializes the system. The food items are described by foods, cuisines and ratings, all of which have a length of n.
foods[i] is the name of the ith food,cuisines[i] is the type of cuisine of the ith food, andratings[i] is the initial rating of the ith food.void changeRating(String food, int newRating) Changes the rating of the food item with the name food.String highestRated(String cuisine) Returns the name of the food item that has the highest rating for the given type of cuisine. If there is a tie, return the item with the lexicographically smaller name.Note that a string x is lexicographically smaller than string y if x comes before y in dictionary order, that is, either x is a prefix of y, or if i is the first position such that x[i] != y[i], then x[i] comes before y[i] in alphabetic order.
Example 1:
Input
["FoodRatings", "highestRated", "highestRated", "changeRating", "highestRated", "changeRating", "highestRated"]
[[["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]], ["korean"], ["japanese"], ["sushi", 16], ["japanese"], ["ramen", 16], ["japanese"]]
Output
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]
Explanation
FoodRatings foodRatings = new FoodRatings(["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]);
foodRatings.highestRated("korean"); // return "kimchi"
// "kimchi" is the highest rated korean food with a rating of 9.
foodRatings.highestRated("japanese"); // return "ramen"
// "ramen" is the highest rated japanese food with a rating of 14.
foodRatings.changeRating("sushi", 16); // "sushi" now has a rating of 16.
foodRatings.highestRated("japanese"); // return "sushi"
// "sushi" is the highest rated japanese food with a rating of 16.
foodRatings.changeRating("ramen", 16); // "ramen" now has a rating of 16.
foodRatings.highestRated("japanese"); // return "ramen"
// Both "sushi" and "ramen" have a rating of 16.
// However, "ramen" is lexicographically smaller than "sushi".
Constraints:
1 <= n <= 2 * 104n == foods.length == cuisines.length == ratings.length1 <= foods[i].length, cuisines[i].length <= 10foods[i], cuisines[i] consist of lowercase English letters.1 <= ratings[i] <= 108foods are distinct.food will be the name of a food item in the system across all calls to changeRating.cuisine will be a type of cuisine of at least one food item in the system across all calls to highestRated.2 * 104 calls in total will be made to changeRating and highestRated.When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The straightforward approach is to maintain a single, master list of all foods with their details. To find the best food for a certain cuisine, we simply check every single food in the entire list, one by one, to see which one is the best fit according to the rules.
Here's how the algorithm would work step-by-step:
class FoodRatings:
def __init__(self, foods: list[str], cuisines: list[str], ratings: list[int]):
# Create a single master list to hold all food details for straightforward iteration.
self.food_master_list = []
for index in range(len(foods)):
food_name = foods[index]
cuisine_type = cuisines[index]
initial_rating = ratings[index]
self.food_master_list.append([food_name, cuisine_type, initial_rating])
def changeRating(self, food: str, newRating: int) -> None:
# We must search the entire list to find the matching food by its name to update the rating.
for food_record in self.food_master_list:
if food_record[0] == food:
food_record[2] = newRating
return
def highestRated(self, cuisine: str) -> str:
# Initialize a variable to keep track of the best food found so far for the target cuisine.
current_best_food_for_cuisine = None
for food_record in self.food_master_list:
food_name = food_record[0]
food_cuisine = food_record[1]
food_rating = food_record[2]
if food_cuisine == cuisine:
# A food becomes the new winner if its rating is higher, or if tied, its name is smaller.
if current_best_food_for_cuisine is None or \
food_rating > current_best_food_for_cuisine[2] or \
(food_rating == current_best_food_for_cuisine[2] and food_name < current_best_food_for_cuisine[0]):
current_best_food_for_cuisine = food_record
return current_best_food_for_cuisine[0]The optimal strategy avoids searching through all the foods every time. Instead, it maintains a separate, continuously sorted 'leaderboard' for each cuisine, making it instantly possible to find the top-rated food for any given cuisine.
Here's how the algorithm would work step-by-step:
class FoodRatings:
def __init__(self, foods: list[str], cuisines: list[str], ratings: list[int]):
# A map for O(1) access to a food's current rating and cuisine.
self.food_info = {}
self.cuisine_leaderboards = collections.defaultdict(list)
for index in range(len(foods)):
food_name = foods[index]
cuisine_name = cuisines[index]
initial_rating = ratings[index]
self.food_info[food_name] = [initial_rating, cuisine_name]
# Storing ratings as negative allows a min-heap to function as a max-heap.
heapq.heappush(self.cuisine_leaderboards[cuisine_name], (-initial_rating, food_name))
def changeRating(self, food_to_update: str, new_rating: int) -> None:
original_cuisine = self.food_info[food_to_update][1]
self.food_info[food_to_update][0] = new_rating
# This lazy-add avoids a costly search-and-remove from the heap for the old rating.
heapq.heappush(self.cuisine_leaderboards[original_cuisine], (-new_rating, food_to_update))
def highestRated(self, cuisine_to_query: str) -> str:
leaderboard_for_cuisine = self.cuisine_leaderboards[cuisine_to_query]
# Stale ratings might be at the top, so we must find the first valid one.
while -leaderboard_for_cuisine[0][0] != self.food_info[leaderboard_for_cuisine[0][1]][0]:
heapq.heappop(leaderboard_for_cuisine)
return leaderboard_for_cuisine[0][1]| Case | How to Handle |
|---|---|
| Multiple food items in a cuisine share the same maximum rating. | A custom comparator for the priority queue ensures ties in rating are broken by choosing the lexicographically smaller food name. |
| A single food item's rating is changed multiple times consecutively. | The lazy deletion strategy causes outdated entries to build up in the heap, which are filtered out during the next relevant `highestRated` query. |
| A single cuisine contains almost all food items, while other cuisines contain only one or a few. | Performance for `highestRated` is proportional to the logarithm of the number of foods in the queried cuisine, gracefully handling the skewed distribution. |
| The system is initialized with the maximum number of foods and receives the maximum number of method calls. | The use of hash maps and heaps provides logarithmic time complexity for operations, ensuring the solution scales to meet the maximum constraints. |
| The system is initialized with only one food item for a given cuisine. | The data structures handle the base case of a single item correctly, with `highestRated` for its cuisine always returning that one item. |
| changeRating is called with a newRating that is identical to the food's current rating. | The logic still functions correctly by adding a new entry to the heap, which does not alter the correctness of `highestRated` calls. |
| Food names involved in a tie-breaking situation have different lengths or are prefixes of each other. | The solution leverages built-in string comparison functions which properly handle prefixes and different lengths according to standard lexicographical rules. |
| A rating is changed to a lower value, potentially making a different food item the new highest rated. | The new, lower-rated entry is added to the heap, and the old, higher-rated entry is invalidated and will be ignored by future `highestRated` calls. |