LeetCode 2353. 设计食物评分系统

2353. 设计食物评分系统

设计一个支持下述操作的食物评分系统:

  • 修改 系统中列出的某种食物的评分。
  • 返回系统中某一类烹饪方式下评分最高的食物。

实现 FoodRatings 类:

  • FoodRatings(String[] foods, String[] cuisines, int[] ratings) 初始化系统。食物由 foodscuisines 和 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. 设计数字容器系统-CSDN博客

我们可以用一个哈希表 fmap 记录每个食物名称对应的食物评分和烹饪方式,另一个哈希表套平衡树 cmap 记录每个烹饪方式对应的食物评分和食物名字集合。

对于 changeRating 操作,先从 cmap[fmap[food].cuisine] 中删掉旧数据,然后将 newRating 和 food 记录到 cmap 和 fmap 中。

  1. 数据结构选择:首先,我们选用两个哈希表(HashMap或Python的defaultdict)来存储相关信息。fmap用于存储每种食物的名称映射到其评分和烹饪方式的元组;cmap则用于存储每种烹饪方式映射到一个有序集合,集合中的元素是食物名称和其评分的元组。

  2. 初始化过程:在 FoodRatings类的构造函数中,我们遍历输入的食物列表,将食物名称、评分和烹饪方式存入fmap。同时,对于每种烹饪方式,我们创建一个有序集合(TreeSetSortedSet),并根据食物的评分进行排序。如果评分相同,则按字典序排序。这样,每个烹饪方式下的有序集合都以评分最高的元素在前。

  3. 修改评分操作:在changeRating方法中,我们首先从fmap中检索出要修改评分的食物当前的评分和烹饪方式。然后,从cmap中对应的有序集合里移除旧的评分和食物名称的元组,再将更新后的评分和食物名称的元组添加到有序集合中。这一步骤保证了有序集合中的食物始终按照评分降序排列。

  4. 查询最高评分操作:在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)

复杂度分析

时间复杂度

  1. 初始化 (FoodRatings 构造函数): 初始化过程需要遍历所有食物项,将它们存储到 fmapcmap 中。对于每个食物项,除了哈希表的插入操作(平均时间复杂度为 O(1)),还需要将食物项插入到对应的 TreeSetSortedSet 中。TreeSet 的插入操作时间复杂度为 O(log m),其中 m 是特定菜系中食物项的数量。因此,整体初始化的时间复杂度为 O(n log m),其中 n 是总食物项的数量。

  2. 修改评分 (changeRating 方法): 当修改某个食物的评分时,需要从 cmap 中对应的 TreeSet 移除旧的评分和食物名称的元组,然后插入新的元组。由于 TreeSet 是一个有序集合,移除和插入操作的时间复杂度都是 O(log m)。因此,修改评分操作的时间复杂度为 O(log m)。

  3. 查询最高评分 (highestRated 方法): 查询操作只需要访问 cmap 中对应菜系的 TreeSet 的第一个元素,这是一个常数时间的操作,时间复杂度为 O(1)。

空间复杂度

  1. 初始化: 需要为每个食物项在 fmap 中存储一个键值对,以及在 cmap 中为每个菜系维护一个 TreeSet。每个 TreeSet 存储了该菜系所有食物项的元组。因此,空间复杂度主要取决于食物项的数量 n 和菜系中食物项数量的最大值 m。最坏情况下,每个食物项属于不同的菜系,此时 cmap 中的 TreeSet 总数将接近 n。每个 TreeSet 存储的是一个食物项的元组,因此空间复杂度为 O(n)。

  2. 修改评分: 修改操作不影响额外的空间使用,因为它只涉及在已有的 TreeSet 中替换元素。

  3. 查询最高评分: 查询操作同样不影响额外的空间使用,因为它只涉及访问已有的 TreeSet

解法2:懒删除堆

算法逻辑

  1. 懒删除策略:在优先队列中,我们不立即删除或更新已修改评分的食物元组,而是在查询操作中进行懒删除,即当查询到顶部元素与fmap中的评分不一致时,才将其从优先队列中移除。

  2. 优先队列的使用:优先队列(最小堆)用于快速访问具有最高评分的食物,堆顶始终是当前评分最高的元素。

  3. 更新操作的优化:在changeRating中,我们不需要从堆中删除旧的评分,只需添加新的评分。这样可以避免删除操作的开销。

  4. 查询操作的优化:在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),需要存储所有食物的评分和烹饪方式信息。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值