Quick Overview

Implement an ad service with InsertAd and GetAd, where each GetAd returns the highest-scoring ad, never repeats the ad served by the previous call, and lowers the served ad's score by one. It tests priority-queue design, deterministic tie-breaking and handling calls where no ad is eligible.

Ad Service That Serves the Top-Scoring Ad Without Back-to-Back Repeats

Company: Google

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Onsite

Implement an ad service with two operations: - `InsertAd(content, score)` adds an ad with text `content` (a string) and an integer `score`. - `GetAd()` returns one ad, subject to three requirements: it must return the ad whose current score is highest; no ad may be returned by two consecutive `GetAd` calls; and the returned ad's score then decreases by 1. The candidate's report states only these three requirements. The rules below also fix ties, what happens when no ad qualifies, and what "consecutive" means, so that every sequence of operations has exactly one correct output. For this console, the service is driven by a list of operations. ### Function Signature ```python def run_ad_service(operations: list[list[str | int]]) -> list[str | None]: ``` ### Rules - The service starts empty, and the operations are applied in order. - `["InsertAd", content, score]` adds a new ad and produces no output. - `["GetAd"]` considers every stored ad except the one returned by the immediately preceding `GetAd` operation. If there was no preceding `GetAd`, or it returned `None`, no ad is excluded. `InsertAd` operations between two `GetAd` operations do not change this. - Among the ads it considers, `GetAd` returns the `content` of the one with the highest current score; if several share that score, the one inserted earliest wins. That ad's score then decreases by 1. - If there is no ad to consider (the service is empty, or its only ad was returned by the preceding `GetAd`), `GetAd` returns `None` and no score changes. - Ads are never removed. A score may fall to zero or below, and the ad stays eligible. - Return a list with one entry per `GetAd` operation, in order. ### Constraints - `1 <= len(operations) <= 10^5` - `-10^9 <= score <= 10^9` when an ad is inserted. Each `GetAd` lowers one score by 1, so every score stays within `[-10^9 - 10^5, 10^9]`. - `content` is 1 to 20 lowercase English letters, and every `InsertAd` uses a different `content`. ### Examples **Example 1** ```text Input: operations = [["InsertAd", "shoes", 3], ["InsertAd", "games", 3], ["InsertAd", "books", 2], ["GetAd"], ["GetAd"], ["GetAd"], ["GetAd"], ["GetAd"], ["GetAd"]] Output: ["shoes", "games", "shoes", "games", "books", "shoes"] ``` | GetAd call | Scores before the call (shoes, games, books) | Excluded | Returned | | --- | --- | --- | --- | | 1 | 3, 3, 2 | none | shoes (ties with games, inserted earlier) | | 2 | 2, 3, 2 | shoes | games | | 3 | 2, 2, 2 | games | shoes (ties with books, inserted earlier) | | 4 | 1, 2, 2 | shoes | games (ties with books, inserted earlier) | | 5 | 1, 1, 2 | games | books | | 6 | 1, 1, 1 | books | shoes (ties with games, inserted earlier) | **Example 2** ```text Input: operations = [["GetAd"], ["InsertAd", "solo", 1], ["GetAd"], ["GetAd"], ["GetAd"], ["InsertAd", "late", 0], ["GetAd"], ["GetAd"]] Output: [None, "solo", None, "solo", "late", "solo"] ``` The first call finds no ads. The second returns "solo" (score 1 to 0). The third must exclude "solo", has nothing else to consider and returns `None`. Because that call returned `None`, the fourth may return "solo" again (score 0 to -1). After "late" is inserted with score 0, the fifth call excludes "solo" and returns "late" (score 0 to -1); the sixth excludes "late" and returns "solo" even though its score is -1.

Overview: Implement an ad service with InsertAd and GetAd, where each GetAd returns the highest-scoring ad, never repeats the ad served by the previous call, and lowers the served ad's score by one. It tests priority-queue design, deterministic tie-breaking and handling calls where no ad is eligible.

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

Design an ad service that supports two operations: - `InsertAd(content, score)` adds an ad whose text is `content` (a string) and whose score is the integer `score`. - `GetAd()` returns one ad: it must return the ad whose current score is highest, no ad may be returned by two consecutive `GetAd` calls, and the returned ad's score then decreases by 1. The service is driven by a list of operations. Implement `run_ad_service(operations)`: start with an empty service, apply the operations in order, and return the results of the `GetAd` operations. **Rules** - `["InsertAd", content, score]` adds a new ad and produces no output. - `["GetAd"]` considers every stored ad except the one returned by the immediately preceding `GetAd` operation. If there was no preceding `GetAd`, or it returned `None`, no ad is excluded. `InsertAd` operations between two `GetAd` operations do not change this. - Among the ads it considers, `GetAd` returns the `content` of the one with the highest current score; if several share that score, the one inserted earliest wins. That ad's score then decreases by 1. - If there is no ad to consider (the service is empty, or its only ad was returned by the preceding `GetAd`), `GetAd` returns `None` and no score changes. - Ads are never removed. A score may fall to zero or below, and the ad stays eligible. - Return a list with one entry per `GetAd` operation, in order. If there is no `GetAd` operation, return an empty list. `None` is `null` in JavaScript and Java and `std::nullopt` (an empty `std::optional<std::string>`) in C++. In Java each operation is a `java.util.List<Object>` (read the score through `java.lang.Number`); in C++ each operation is a `std::vector<std::string>` and the score is given as its decimal text (for example `"-5"`). **Example 1** ```text Input: operations = [["InsertAd", "shoes", 3], ["InsertAd", "games", 3], ["InsertAd", "books", 2], ["GetAd"], ["GetAd"], ["GetAd"], ["GetAd"], ["GetAd"], ["GetAd"]] Output: ["shoes", "games", "shoes", "games", "books", "shoes"] ``` | GetAd call | Scores before the call (shoes, games, books) | Excluded | Returned | | --- | --- | --- | --- | | 1 | 3, 3, 2 | none | shoes (ties with games, inserted earlier) | | 2 | 2, 3, 2 | shoes | games | | 3 | 2, 2, 2 | games | shoes (ties with books, inserted earlier) | | 4 | 1, 2, 2 | shoes | games (ties with books, inserted earlier) | | 5 | 1, 1, 2 | games | books | | 6 | 1, 1, 1 | books | shoes (ties with games, inserted earlier) | **Example 2** ```text Input: operations = [["GetAd"], ["InsertAd", "solo", 1], ["GetAd"], ["GetAd"], ["GetAd"], ["InsertAd", "late", 0], ["GetAd"], ["GetAd"]] Output: [None, "solo", None, "solo", "late", "solo"] ``` The first call finds no ads. The second returns "solo" (score 1 to 0). The third must exclude "solo", has nothing else to consider and returns `None`. Because that call returned `None`, the fourth may return "solo" again (score 0 to -1). After "late" is inserted with score 0, the fifth call excludes "solo" and returns "late" (score 0 to -1); the sixth excludes "late" and returns "solo" even though its score is -1. **Constraints** - `1 <= len(operations) <= 10^5` - Each operation is either `["InsertAd", content, score]` or `["GetAd"]`. - `-10^9 <= score <= 10^9` when an ad is inserted. Each `GetAd` lowers one score by 1, so every score stays within `[-10^9 - 10^5, 10^9]`. No score ever exceeds 2^31 - 1 in absolute value, so every score fits in a signed 32-bit integer. - `content` is 1 to 20 lowercase English letters, and every `InsertAd` uses a different `content`.

Constraints

  • 1 <= len(operations) <= 10^5
  • Each operation is either ["InsertAd", content, score] or ["GetAd"]
  • -10^9 <= score <= 10^9 when an ad is inserted; each GetAd lowers one score by 1, so every score stays within [-10^9 - 10^5, 10^9] (fits in a signed 32-bit integer)
  • content is 1 to 20 lowercase English letters, and every InsertAd uses a different content

Examples

Input: ([['InsertAd', 'shoes', 3], ['InsertAd', 'games', 3], ['InsertAd', 'books', 2], ['GetAd'], ['GetAd'], ['GetAd'], ['GetAd'], ['GetAd'], ['GetAd']],)

Expected Output: ['shoes', 'games', 'shoes', 'games', 'books', 'shoes']

Explanation: Source Example 1: ties at insertion and ties created by decrements go to the earliest-inserted ad; the previous result is always excluded.

Input: ([['GetAd'], ['InsertAd', 'solo', 1], ['GetAd'], ['GetAd'], ['GetAd'], ['InsertAd', 'late', 0], ['GetAd'], ['GetAd']],)

Expected Output: [None, 'solo', None, 'solo', 'late', 'solo']

Explanation: Source Example 2: the empty service gives None, a None result clears the exclusion, and a score of -1 stays eligible.

Hints

  1. Track which ad the previous GetAd returned. If that call returned None, or there was no earlier GetAd, nothing is excluded, and an InsertAd never resets the exclusion.
  2. Rank ads by current score and break ties by insertion order. Only one ad can be excluded at a time, so the ad you return is always one of the top two in that ranking.
  3. Ads are never removed and scores can drop to zero or below, so a negative-score ad may still be the only one left to return.

Loading coding console...

Show the approach

Approach

Keep one priority-queue entry per ad, ordered by higher current score first and earlier insertion first, which is exactly the order GetAd uses to choose. Only the ad a GetAd returns changes its score, and that ad is popped at that moment and pushed back with score - 1, so every entry always holds its ad's current score and there are no stale entries. Also remember the insertion index of the ad returned by the previous GetAd, or no ad.

GetAd: if the queue is empty, return None and clear the exclusion. Otherwise pop the best entry. If it is not the excluded ad, it is the answer. If it is the excluded ad, then because at most one ad is excluded, the best eligible ad is the next entry: pop it as the answer and push the excluded entry back unchanged (an excluded ad's score does not drop). If no other entry exists, push the excluded entry back, return None and clear the exclusion, so the next GetAd may return that ad again. After choosing an ad, append its content, push it back with score - 1 and record it as the excluded ad. InsertAd only pushes a new entry and leaves the exclusion as it is.

Correctness: the queue order matches the rule's ranking (score, then insertion order), and the best ad that is not excluded is either the queue's top or, when the top is excluded, the second entry. Edge cases: no GetAd gives [], GetAd on an empty service gives None, a lone ad alternates with None, ties go to the earliest insertion, and zero or negative scores (down to -10^9 - 10^5, inside 32-bit range) stay eligible.

Time complexity:
O(n log n)
Space complexity:
O(n)