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
- 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.
- 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.
- 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.