设计一个支持下述操作的食物评分系统:
- 修改 系统中列出的某种食物的评分。
- 返回系统中某一类烹饪方式下评分最高的食物。
实现 FoodRatings
类:
FoodRatings(String[] foods, String[] cuisines, int[] ratings)
初始化系统。食物由foods
、cuisines
和ratings
描述,长度均为n
。foods[i]
是第i
种食物的名字。cuisines[i]
是第i
种食物的烹饪方式。ratings[i]
是第i
种食物的最初评分。
void changeRating(String food, int newRating)
修改名字为food
的食物的评分。String highestRated(String cuisine)
返回指定烹饪方式cuisine
下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。
注意,字符串 x
的字典序比字符串 y
更小的前提是:x
在字典中出现的位置在 y
之前,也就是说,要么 x
是 y
的前缀,或者在满足 x[i] != y[i]
的第一个位置 i
处,x[i]
在字母表中出现的位置在 y[i]
之前。
示例:
输入 ["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"]] 输出 [null, "kimchi", "ramen", null, "sushi", null, "ramen"] 解释 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"); // 返回 "kimchi" // "kimchi" 是分数最高的韩式料理,评分为 9 。 foodRatings.highestRated("japanese"); // 返回 "ramen" // "ramen" 是分数最高的日式料理,评分为 14 。 foodRatings.changeRating("sushi", 16); // "sushi" 现在评分变更为 16 。 foodRatings.highestRated("japanese"); // 返回 "sushi" // "sushi" 是分数最高的日式料理,评分为 16 。 foodRatings.changeRating("ramen", 16); // "ramen" 现在评分变更为 16 。 foodRatings.highestRated("japanese"); // 返回 "ramen" // "sushi" 和 "ramen" 的评分都是 16 。 // 但是,"ramen" 的字典序比 "sushi" 更小。
提示:
1 <= n <= 2 * 10^4
n == foods.length == cuisines.length == ratings.length
1 <= foods[i].length, cuisines[i].length <= 10
foods[i]
、cuisines[i]
由小写英文字母组成1 <= ratings[i] <= 10^8
foods
中的所有字符串 互不相同- 在对
changeRating
的所有调用中,food
是系统中食物的名字。 - 在对
highestRated
的所有调用中,cuisine
是系统中 至少一种 食物的烹饪方式。 - 最多调用
changeRating
和highestRated
总计2 * 10^4
次
提示 1
The key to solving this problem is to properly store the data using the right data structures.
提示 2
Firstly, a hash table is needed to efficiently map each food item to its cuisine and current rating.
提示 3
In addition, another hash table is needed to map cuisines to foods within each cuisine stored in an ordered set according to their ratings.
解法1:平衡树(有序集合)
同类题型:LeetCode 2349. 设计数字容器系统-优快云博客
我们可以用一个哈希表 fmap 记录每个食物名称对应的食物评分和烹饪方式,另一个哈希表套平衡树 cmap 记录每个烹饪方式对应的食物评分和食物名字集合。
对于 changeRating 操作,先从 cmap[fmap[food].cuisine] 中删掉旧数据,然后将 newRating 和 food 记录到 cmap 和 fmap 中。
-
数据结构选择:首先,我们选用两个哈希表(
HashMap
或Python的defaultdict
)来存储相关信息。fmap
用于存储每种食物的名称映射到其评分和烹饪方式的元组;cmap
则用于存储每种烹饪方式映射到一个有序集合,集合中的元素是食物名称和其评分的元组。 -
初始化过程:在
FoodRatings
类的构造函数中,我们遍历输入的食物列表,将食物名称、评分和烹饪方式存入fmap
。同时,对于每种烹饪方式,我们创建一个有序集合(TreeSet
或SortedSet
),并根据食物的评分进行排序。如果评分相同,则按字典序排序。这样,每个烹饪方式下的有序集合都以评分最高的元素在前。 -
修改评分操作:在
changeRating
方法中,我们首先从fmap
中检索出要修改评分的食物当前的评分和烹饪方式。然后,从cmap
中对应的有序集合里移除旧的评分和食物名称的元组,再将更新后的评分和食物名称的元组添加到有序集合中。这一步骤保证了有序集合中的食物始终按照评分降序排列。 -
查询最高评分操作:在
highestRated
方法中,我们直接访问cmap
中指定烹饪方式对应的有序集合的第一个元素。由于集合是按评分降序排列的,第一个元素即为评分最高的食物名称。如果存在评分并列的情况,由于字典序的原因,集合中的食物名称顺序已经是按评分和字典序排序的,因此返回的第一个元素就是正确答案。
Java版:
class FoodRatings {
Map<String, Pair<Integer, String>> fmap;
Map<String, TreeSet<Pair<Integer, String>>> cmap;
public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
fmap = new HashMap<>();
cmap = new HashMap<>();
for (int i = 0; i < ratings.length; i++) {
int r = ratings[i];
String f = foods[i];
String c = cuisines[i];
fmap.put(f, new Pair<>(r, c));
cmap.putIfAbsent(c, new TreeSet<>((a, b) -> {
return a.getKey().equals(b.getKey()) ? a.getValue().compareTo(b.getValue()) : b.getKey() - a.getKey();
}));
cmap.get(c).add(new Pair<>(r, f));
}
}
public void changeRating(String food, int newRating) {
int r = fmap.get(food).getKey();
String c = fmap.get(food).getValue();
cmap.get(c).remove(new Pair<>(r, food));
cmap.get(c).add(new Pair<>(newRating, food));
fmap.put(food, new Pair<>(newRating, c));
}
public String highestRated(String cuisine) {
return cmap.get(cuisine).first().getValue();
}
}
/**
* Your FoodRatings object will be instantiated and called as such:
* FoodRatings obj = new FoodRatings(foods, cuisines, ratings);
* obj.changeRating(food,newRating);
* String param_2 = obj.highestRated(cuisine);
*/
Python3版:
from sortedcontainers import SortedSet
class FoodRatings:
def __init__(self, foods: List[str], cuisines: List[str], ratings: List[int]):
self.fmap = defaultdict()
self.cmap = defaultdict(SortedSet)
for f, c, r in zip(foods, cuisines, ratings):
self.fmap[f] = (r, c)
self.cmap[c].add((-r, f))
def changeRating(self, food: str, newRating: int) -> None:
r = self.fmap[food][0]
c = self.fmap[food][1]
self.cmap[c].remove((-r, food))
self.cmap[c].add((-newRating, food))
self.fmap[food] = (newRating, c)
def highestRated(self, cuisine: str) -> str:
return self.cmap[cuisine][0][1]
# Your FoodRatings object will be instantiated and called as such:
# obj = FoodRatings(foods, cuisines, ratings)
# obj.changeRating(food,newRating)
# param_2 = obj.highestRated(cuisine)
复杂度分析
时间复杂度
-
初始化 (
FoodRatings
构造函数): 初始化过程需要遍历所有食物项,将它们存储到fmap
和cmap
中。对于每个食物项,除了哈希表的插入操作(平均时间复杂度为 O(1)),还需要将食物项插入到对应的TreeSet
或SortedSet
中。TreeSet
的插入操作时间复杂度为 O(log m),其中 m 是特定菜系中食物项的数量。因此,整体初始化的时间复杂度为 O(n log m),其中 n 是总食物项的数量。 -
修改评分 (
changeRating
方法): 当修改某个食物的评分时,需要从cmap
中对应的TreeSet
移除旧的评分和食物名称的元组,然后插入新的元组。由于TreeSet
是一个有序集合,移除和插入操作的时间复杂度都是 O(log m)。因此,修改评分操作的时间复杂度为 O(log m)。 -
查询最高评分 (
highestRated
方法): 查询操作只需要访问cmap
中对应菜系的TreeSet
的第一个元素,这是一个常数时间的操作,时间复杂度为 O(1)。
空间复杂度
-
初始化: 需要为每个食物项在
fmap
中存储一个键值对,以及在cmap
中为每个菜系维护一个TreeSet
。每个TreeSet
存储了该菜系所有食物项的元组。因此,空间复杂度主要取决于食物项的数量 n 和菜系中食物项数量的最大值 m。最坏情况下,每个食物项属于不同的菜系,此时cmap
中的TreeSet
总数将接近 n。每个TreeSet
存储的是一个食物项的元组,因此空间复杂度为 O(n)。 -
修改评分: 修改操作不影响额外的空间使用,因为它只涉及在已有的
TreeSet
中替换元素。 -
查询最高评分: 查询操作同样不影响额外的空间使用,因为它只涉及访问已有的
TreeSet
。
解法2:懒删除堆
算法逻辑
-
懒删除策略:在优先队列中,我们不立即删除或更新已修改评分的食物元组,而是在查询操作中进行懒删除,即当查询到顶部元素与
fmap
中的评分不一致时,才将其从优先队列中移除。 -
优先队列的使用:优先队列(最小堆)用于快速访问具有最高评分的食物,堆顶始终是当前评分最高的元素。
-
更新操作的优化:在
changeRating
中,我们不需要从堆中删除旧的评分,只需添加新的评分。这样可以避免删除操作的开销。 -
查询操作的优化:在
highestRated
中,通过懒删除策略,我们确保了查询操作的效率,避免了在每次修改评分时都进行堆的更新。
Java版:
class FoodRatings {
Map<String, Pair<Integer, String>> fmap;
Map<String, Queue<Pair<Integer, String>>> cmap;
public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
fmap = new HashMap<>();
cmap = new HashMap<>();
for (int i = 0; i < foods.length; i++) {
String f = foods[i];
String c = cuisines[i];
int r = ratings[i];
fmap.put(f, new Pair<>(r, c));
cmap.putIfAbsent(c, new PriorityQueue<>((a, b) -> {
return a.getKey().equals(b.getKey()) ? a.getValue().compareTo(b.getValue()) : b.getKey() - a.getKey();
}));
cmap.get(c).offer(new Pair<>(r, f));
}
}
public void changeRating(String food, int newRating) {
String c = fmap.get(food).getValue();
cmap.get(c).add(new Pair<>(newRating, food));
fmap.put(food, new Pair<>(newRating, c));
}
public String highestRated(String cuisine) {
while ( !fmap.get(cmap.get(cuisine).peek().getValue()).getKey().equals(cmap.get(cuisine).peek().getKey()) ) {
cmap.get(cuisine).poll();
}
return cmap.get(cuisine).peek().getValue();
}
}
/**
* Your FoodRatings object will be instantiated and called as such:
* FoodRatings obj = new FoodRatings(foods, cuisines, ratings);
* obj.changeRating(food,newRating);
* String param_2 = obj.highestRated(cuisine);
*/
Python3版:
class FoodRatings:
def __init__(self, foods: List[str], cuisines: List[str], ratings: List[int]):
self.fmap = defaultdict()
self.cmap = defaultdict(list)
for f, c, r in zip(foods, cuisines, ratings):
self.fmap[f] = [r, c]
heappush(self.cmap[c], [-r, f])
def changeRating(self, food: str, newRating: int) -> None:
c = self.fmap[food][1]
heappush(self.cmap[c], [-newRating, food])
self.fmap[food][0] = newRating
def highestRated(self, cuisine: str) -> str:
while self.fmap[self.cmap[cuisine][0][1]][0] != -self.cmap[cuisine][0][0]:
heappop(self.cmap[cuisine])
return self.cmap[cuisine][0][1]
# Your FoodRatings object will be instantiated and called as such:
# obj = FoodRatings(foods, cuisines, ratings)
# obj.changeRating(food,newRating)
# param_2 = obj.highestRated(cuisine)
复杂度分析
-
时间复杂度:
- 初始化:O(n),其中n是食物的数量。
- 修改评分:O(log m),其中m是特定烹饪方式下的食物数量。添加操作的时间复杂度为O(log m)。
- 查询最高评分:O(log m),最坏情况下可能需要执行懒删除操作,每次删除操作的时间复杂度为O(log m)。
-
空间复杂度:O(n),需要存储所有食物的评分和烹饪方式信息。