Quick Overview

Process a sequence of rating updates and highest-rated queries over foods grouped by cuisine, returning each query's top food with ties broken by the lexicographically smallest name. It tests designing data structures that keep queries fast and correct while ratings move both up and down.

Highest-rated food per cuisine under rating updates with a lexicographic tie-break

Company: Apple

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

A rating tracker is created from a list of foods. Each food belongs to one cuisine and has an integer rating. The tracker then processes a sequence of operations in order: an update sets one food's rating to a new value, and a query asks for the highest-rated food of a cuisine at that moment. Return the answers to all queries. The report describes the task as similar to a well-known design problem: a class with a constructor, an update method and a query method, written together with the candidate's own test cases. This version expresses the same behaviour as one function over a list of operations and fixes the exact rules below. ### Function Signature ```python def run_rating_tracker(foods: list[str], cuisines: list[str], ratings: list[int], operations: list[list[str]]) -> list[str]: ``` ### Rules - Food `foods[i]` belongs to cuisine `cuisines[i]` and starts with rating `ratings[i]`. Food names are distinct, and a food's cuisine never changes. - `["change", food, r]` sets the current rating of `food` to the integer written in decimal in the string `r`. It produces no output. The new rating may be higher than, lower than or equal to the old one. - `["highest", cuisine]` outputs the name of the food with the highest current rating among the foods of `cuisine`. If several of them share that rating, it outputs the lexicographically smallest of their names. - A query reflects every change that comes before it in `operations` and none that come after it. Queries never modify the tracker, so repeating a query with no change in between gives the same answer. - Return the outputs of all `"highest"` operations in the order they occur, or an empty list if there are none. ### Constraints - `1 <= n <= 2 * 10^4`, where `n = len(foods) = len(cuisines) = len(ratings)`. - Every food name and cuisine name consists of 1 to 10 lowercase English letters. - `1 <= rating <= 10^8` for initial and changed ratings; `r` has no sign and no leading zeros. - `1 <= len(operations) <= 2 * 10^4`; each operation is either `["change", food, r]` or `["highest", cuisine]`. - Every `change` names a food in `foods`, and every `highest` names a cuisine that appears in `cuisines`. - Lexicographic order is ordinary string comparison: compare letter by letter, and a name that is a prefix of another comes first. ### Examples **Example 1** ```text Input: foods = ["pho", "banhmi", "tacos", "burrito", "goi"] cuisines = ["vietnamese", "vietnamese", "mexican", "mexican", "vietnamese"] ratings = [7, 9, 8, 8, 9] operations = [["highest", "vietnamese"], ["highest", "mexican"], ["change", "pho", "10"], ["highest", "vietnamese"], ["change", "pho", "3"], ["highest", "vietnamese"]] Output: ["banhmi", "burrito", "pho", "banhmi"] ``` "banhmi" and "goi" share the top Vietnamese rating of 9, and "banhmi" is smaller; "burrito" and "tacos" tie at 8. After "pho" rises to 10 it leads, and after it drops to 3 the tie between "banhmi" and "goi" decides again. **Example 2** ```text Input: foods = ["dal", "naan", "korma"] cuisines = ["indian", "indian", "indian"] ratings = [5, 5, 4] operations = [["change", "dal", "2"], ["highest", "indian"], ["change", "naan", "1"], ["highest", "indian"], ["highest", "indian"], ["change", "dal", "5"], ["highest", "indian"]] Output: ["naan", "korma", "korma", "dal"] ``` After "dal" drops to 2, "naan" (5) leads. After "naan" drops to 1, "korma" (4) leads, and the repeated query returns it again. Raising "dal" back to 5 puts it on top. **Example 3** ```text Input: foods = ["zeppole", "arancini"] cuisines = ["italian", "italian"] ratings = [6, 3] operations = [["highest", "italian"], ["change", "arancini", "6"], ["highest", "italian"], ["change", "zeppole", "6"], ["highest", "italian"]] Output: ["zeppole", "arancini", "arancini"] ``` Raising "arancini" to 6 creates a tie that it wins by name; setting "zeppole" to its current rating changes nothing.

Overview: Process a sequence of rating updates and highest-rated queries over foods grouped by cuisine, returning each query's top food with ties broken by the lexicographically smallest name. It tests designing data structures that keep queries fast and correct while ratings move both up and down.

Read the full Apple Software Engineer interview experience this question came from

A rating tracker is built from a list of foods. Food `foods[i]` belongs to cuisine `cuisines[i]` and starts with the integer rating `ratings[i]`. Food names are distinct, and a food's cuisine never changes. The tracker then processes `operations` in order. Each operation is one of: - `["change", food, r]`: set the current rating of `food` to the integer written in decimal in the string `r`. It produces no output. The new rating may be higher than, lower than or equal to the old one. - `["highest", cuisine]`: output the name of the food with the highest current rating among the foods of `cuisine`. If several of them share that rating, output the lexicographically smallest of their names. A query reflects every change that comes before it in `operations` and none that come after it. Queries never modify the tracker, so repeating a query with no change in between gives the same answer. Implement `run_rating_tracker(foods, cuisines, ratings, operations)`. Return the outputs of all `"highest"` operations, as a list of strings in the order they occur, or an empty list if there are none. Lexicographic order is ordinary string comparison: compare letter by letter, and a name that is a prefix of another comes first. ### Example 1 ```text Input: foods = ["pho", "banhmi", "tacos", "burrito", "goi"] cuisines = ["vietnamese", "vietnamese", "mexican", "mexican", "vietnamese"] ratings = [7, 9, 8, 8, 9] operations = [["highest", "vietnamese"], ["highest", "mexican"], ["change", "pho", "10"], ["highest", "vietnamese"], ["change", "pho", "3"], ["highest", "vietnamese"]] Output: ["banhmi", "burrito", "pho", "banhmi"] ``` "banhmi" and "goi" share the top Vietnamese rating of 9, and "banhmi" is smaller; "burrito" and "tacos" tie at 8. After "pho" rises to 10 it leads, and after it drops to 3 the tie between "banhmi" and "goi" decides again. ### Example 2 ```text Input: foods = ["zeppole", "arancini"] cuisines = ["italian", "italian"] ratings = [6, 3] operations = [["highest", "italian"], ["change", "arancini", "6"], ["highest", "italian"], ["change", "zeppole", "6"], ["highest", "italian"]] Output: ["zeppole", "arancini", "arancini"] ``` Raising "arancini" to 6 creates a tie that it wins by name; setting "zeppole" to its current rating changes nothing. ### Constraints - `1 <= n <= 2 * 10^4`, where `n = len(foods) = len(cuisines) = len(ratings)`. - Food names are distinct. - Every food name and cuisine name consists of 1 to 10 lowercase English letters. - `1 <= rating <= 10^8` for initial and changed ratings; `r` has no sign and no leading zeros. - `1 <= len(operations) <= 2 * 10^4`; each operation is either `["change", food, r]` or `["highest", cuisine]`. - Every `change` names a food in `foods`, and every `highest` names a cuisine that appears in `cuisines`. - Every rating fits in a signed 32-bit integer (no value exceeds 2^31 - 1).

Constraints

  • 1 <= n <= 2 * 10^4, where n = len(foods) = len(cuisines) = len(ratings).
  • Food names are distinct.
  • Every food name and cuisine name consists of 1 to 10 lowercase English letters.
  • 1 <= rating <= 10^8 for initial and changed ratings; r has no sign and no leading zeros.
  • 1 <= len(operations) <= 2 * 10^4; each operation is either ["change", food, r] or ["highest", cuisine].
  • Every change names a food in foods, and every highest names a cuisine that appears in cuisines.
  • Every rating fits in a signed 32-bit integer (no value exceeds 2^31 - 1).

Examples

Input: (['pho', 'banhmi', 'tacos', 'burrito', 'goi'], ['vietnamese', 'vietnamese', 'mexican', 'mexican', 'vietnamese'], [7, 9, 8, 8, 9], [['highest', 'vietnamese'], ['highest', 'mexican'], ['change', 'pho', '10'], ['highest', 'vietnamese'], ['change', 'pho', '3'], ['highest', 'vietnamese']])

Expected Output: ['banhmi', 'burrito', 'pho', 'banhmi']

Explanation: Source Example 1: ties at 9 and 8 go to the smaller name; pho leads at 10, then the banhmi/goi tie decides again after pho drops to 3.

Input: (['dal', 'naan', 'korma'], ['indian', 'indian', 'indian'], [5, 5, 4], [['change', 'dal', '2'], ['highest', 'indian'], ['change', 'naan', '1'], ['highest', 'indian'], ['highest', 'indian'], ['change', 'dal', '5'], ['highest', 'indian']])

Expected Output: ['naan', 'korma', 'korma', 'dal']

Explanation: Source Example 2: the leader is lowered twice until korma leads, a repeated query returns korma again, and dal restored to 5 leads.

Hints

  1. Each query compares only the foods of one cuisine, so group the foods by cuisine up front.
  2. Rank foods by rating first (higher wins) and by name second (smaller wins), and make sure a food can never win with a rating it no longer has.
  3. One cuisine may hold almost all 2 * 10^4 foods while there are up to 2 * 10^4 queries, so rescanning the whole cuisine on every query can be too slow.

Loading coding console...

Show the approach

Approach

Algorithm: keep each food's current rating and cuisine in maps, and for every cuisine a priority queue of (rating, name) entries ordered by higher rating first and smaller name second. Every food starts with one entry in its cuisine's queue. A change stores the new current rating and pushes a fresh (rating, food) entry into that cuisine's queue, leaving older entries in place (lazy deletion). A query looks at the top of the cuisine's queue and discards entries whose rating differs from that food's current rating until the top entry matches; that food is the answer.

Invariant: every food always has at least one entry equal to its current (rating, name), and any entry whose rating equals the food's current rating describes that food correctly. Discarded entries are exactly ones that no longer describe their food, so after the stale entries on top are removed, the top is the minimum of (-current rating, name) over the cuisine's foods: the highest rating, ties broken by the lexicographically smallest name (ordinary string comparison puts a prefix first).

Correctness of the query rules: a query only discards stale entries and never a valid one, so repeating a query with no change in between returns the same food, and each query reflects exactly the changes made before it. If a rating is changed away and later restored, an older entry with that rating becomes valid again and still describes the food correctly.

Edge cases: a change to the food's current rating only adds a duplicate valid entry; a queried cuisine always contains at least one food, so its queue never empties while searching; with no highest operations the result is an empty list; a change in one cuisine never touches another cuisine's queue. All ratings are at most 10^8, so 32-bit integers suffice. The Java and C++ references keep an ordered set per cuisine instead, removing a food before updating its rating and re-inserting it, which gives the same ordering.

Time complexity:
O((n + q) log(n + q)), where q = len(operations)
Space complexity:
O(n + q)