LeetCode 2353. 设计食物评分系统【设计,哈希表,有序集合;堆+懒删除】1781

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

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

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

实现 FoodRatings 类:

  • FoodRatings(String[] foods, String[] cuisines, int[] ratings) 初始化系统。食物由 foodscuisinesratings 描述,长度均为 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 之前,也就是说,要么 xy 的前缀,或者在满足 x[i] != y[i] 的第一个位置 i 处,x[i] 在字母表中出现的位置在 y[i] 之前。

示例:
输入

java 复制代码
["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"]]

输出

java 复制代码
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]

解释

java 复制代码
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] <= 108
  • foods 中的所有字符串 互不相同
  • 在对 changeRating 的所有调用中,food 是系统中食物的名字。
  • 在对 highestRated 的所有调用中,cuisine 是系统中 至少一种 食物的烹饪方式。
  • 最多调用 changeRatinghighestRated 总计 2 * 10^4

解法1 平衡树

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

对于 changeRating 操作,先从 cs[fs[food].cuisine] 中删除旧数据,然后将 newRatingfood 记录到 c s cs cs 和 f s fs fs 中:

cpp 复制代码
// cpp
class FoodRatings {
private:
    unordered_map<string, pair<int, string>> fs;
    unordered_map<string, set<pair<int, string>>> cs;
public:
    FoodRatings(vector<string>& foods, vector<string>& cuisines, vector<int>& ratings) {
        for (int i = 0, n = foods.size(); i < n; ++i) {
            auto &f = foods[i], &c = cuisines[i];
            int r = ratings[i];
            fs[f] = {r, c};
            cs[c].emplace(-r, f);
        }
    }
    
    void changeRating(string food, int newRating) {
        auto &[r, c] = fs[food];
        auto &s = cs[c];
        s.erase({-r, food}); // 移除旧数据
        s.emplace(-newRating, food); // 添加新数据
        r = newRating;
    }
    
    string highestRated(string cuisine) {
        return cs[cuisine].begin()->second;
    }
};
// java
class FoodRatings {
    Map<String, Pair<Integer, String>> fs = new HashMap<>();
    Map<String, TreeSet<Pair<Integer, String>>> cs = new HashMap<>();

    public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
        for (var i = 0; i < foods.length; ++i) {
            String f = foods[i], c = cuisines[i];
            var r = ratings[i];
            fs.put(f, new Pair<>(r, c));
            cs.computeIfAbsent(c, k -> new TreeSet<>((a, b) -> 
                !Objects.equals(a.getKey(), b.getKey()) ? 
                    b.getKey() - a.getKey() : // 逆序 
                    a.getValue().compareTo(b.getValue())
                )).add(new Pair<>(r, f));
        }
    }
    
    public void changeRating(String food, int newRating) {
        var e = fs.get(food);
        var s = cs.get(e.getValue());
        s.remove(new Pair<>(e.getKey(), food)); // 移除旧数据
        s.add(new Pair<>(newRating, food)); // 添加新数据
        fs.put(food, new Pair<>(newRating, e.getValue()));
    }
    
    public String highestRated(String cuisine) {
        return cs.get(cuisine).first().getValue();
    }
}
// python
from sortedcontainers import SortedSet
class FoodRatings:
    def __init__(self, foods: List[str], cuisines: List[str], ratings: List[int]):
        self.fs = {}
        self.cs = defaultdict(SortedSet)
        for f, c, r in zip(foods, cuisines, ratings):
            self.fs[f] = [r, c]
            self.cs[c].add((-r, f))

    def changeRating(self, food: str, newRating: int) -> None:
        r, c = self.fs[food]
        s = self.cs[c]
        s.remove((-r, food)) # 移除旧数据
        s.add((-newRating, food)) # 添加新数据
        self.fs[food][0] = newRating

    def highestRated(self, cuisine: str) -> str:
        return self.cs[cuisine][0][1]

解法2 懒删除堆

另一种做法是用堆:

  • 对于 changeRating 操作,直接往 c s cs cs 中记录,不做任何删除操作;
  • 对于 h i g h e s t R a t e d highestRated highestRated 操作,查看堆顶的食物评分是否等于其实际值,若不相同则意味着对应的元素已被替换成了其他值,堆顶存的是个垃圾数据,直接弹出堆顶;否则堆顶就是答案。
cpp 复制代码
// cpp
class FoodRatings {
private:
    unordered_map<string, pair<int, string>> fs;
    unordered_map<string, 
        priority_queue<pair<int, string>, vector<pair<int, string>>, greater<>>> cs;
public:
    FoodRatings(vector<string>& foods, vector<string>& cuisines, vector<int>& ratings) {
        for (int i = 0, n = foods.size(); i < n; ++i) {
            auto &f = foods[i], &c = cuisines[i];
            int r = ratings[i];
            fs[f] = {r, c};
            cs[c].emplace(-r, f);
        }
    }
    
    void changeRating(string food, int newRating) {
        auto &[r, c] = fs[food];
        cs[c].emplace(-newRating, food); // 直接添加新数据,后面 highestRated 再删除旧的
        r = newRating;
    }
    
    string highestRated(string cuisine) {
        auto &q = cs[cuisine];
        while (-q.top().first != fs[q.top().second].first) // 堆顶的食物评分不等于其实际值
            q.pop();
        return q.top().second;
    }
};
// java
class FoodRatings {
    Map<String, Pair<Integer, String>> fs = new HashMap<>();
    Map<String, Queue<Pair<Integer, String>>> cs = new HashMap<>();

    public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
        for (var i = 0; i < foods.length; ++i) {
            String f = foods[i], c = cuisines[i];
            var r = ratings[i];
            fs.put(f, new Pair<>(r, c));
            cs.computeIfAbsent(c, k -> new PriorityQueue<>((a, b) -> 
                !Objects.equals(a.getKey(), b.getKey()) ? 
                    b.getKey() - a.getKey() : // 逆序 
                    a.getValue().compareTo(b.getValue())
                )).add(new Pair<>(r, f));
        }
    }
    
    public void changeRating(String food, int newRating) {
        var c = fs.get(food).getValue();
        cs.get(c).offer(new Pair<>(newRating, food)); // 直接添加新数据,后面 highestRated 再删除旧的
        fs.put(food, new Pair<>(newRating, c)); // 记录新评分
    }
    
    public String highestRated(String cuisine) {
        var q = cs.get(cuisine);
        while (!Objects.equals(q.peek().getKey(), fs.get(q.peek().getValue()).getKey()))
            q.poll();
        return q.peek().getValue();
    }
}
// python
class FoodRatings:
    def __init__(self, foods: List[str], cuisines: List[str], ratings: List[int]):
        self.fs = {}
        self.cs = defaultdict(list)
        for f, c, r in zip(foods, cuisines, ratings):
            self.fs[f] = [r, c]
            heappush(self.cs[c], (-r, f))

    def changeRating(self, food: str, newRating: int) -> None:
        f = self.fs[food]
        heappush(self.cs[f[1]], (-newRating, food)) # 直接添加新数据,后面highestRated再删除旧的
        f[0] = newRating

    def highestRated(self, cuisine: str) -> str:
        h = self.cs[cuisine]
        while -h[0][0] != self.fs[h[0][1]][0]: # 堆顶的食物评分!=实际值
            heappop(h)
        return h[0][1]
相关推荐
罗西的思考10 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX11 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面11 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号12 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽12 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI12 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者14 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者14 小时前
【征程6】校准量化中HistogramObserver解析
算法
OuO-214 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵
程序喵大人15 小时前
【C++进阶】STL算法与函数对象 - 04 find、count和any_of把查询写成意图
开发语言·c++·算法