Implement a News Publishing System with Topic Subscriptions and Delivery Limits
Company: Optiver
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
You are building the core delivery engine for a news publishing service. The service continuously receives news items, stores them, and — whenever a publish cycle is triggered — decides which stored news items should be delivered to which subscribers, based on each subscriber's interests and delivery limits.
Implement a system that supports the following four operations.
**`add_subscription(subscriber_id, topics, limit)`**
- Registers a subscription for `subscriber_id`.
- `topics` is a non-empty set of topic strings the subscriber is interested in.
- `limit` is the maximum number of news items this subscriber may receive in a single `publish` cycle.
- If the subscriber already has an active subscription, the old subscription is discarded and replaced, exactly as if `remove_subscription(subscriber_id)` had been called first: any undelivered backlog is dropped, and the new subscription only applies to news that arrives after this call.
**`remove_subscription(subscriber_id)`**
- Removes the subscriber's active subscription. Any news items that were queued for this subscriber but not yet delivered are discarded.
- If the subscriber has no active subscription, this is a no-op.
**`news_received(news_id, topic)`**
- A news item with a unique identifier `news_id` and a single `topic` arrives and is stored.
- Items arrive one at a time; the order of `news_received` calls defines the arrival order.
**`publish()`**
- Runs one delivery cycle and returns the delivery plan for this moment.
- A stored news item is **eligible** for a subscriber if all of the following hold:
1. the item's `topic` is in the subscriber's interest set;
2. the item arrived **after** the subscriber's current subscription was created;
3. the item has not already been delivered to this subscriber in an earlier `publish` cycle.
- Each subscriber receives their eligible items in arrival order (oldest first), up to at most `limit` items in this cycle. Eligible items beyond the limit are **not** dropped — they remain queued and are candidates in future `publish` cycles.
- Returns a list of pairs `(subscriber_id, delivered_news_ids)` sorted by `subscriber_id` in ascending order, where `delivered_news_ids` is the ordered list of news ids delivered to that subscriber in this cycle. Subscribers who receive no items in this cycle are omitted. If no subscriber receives anything, return an empty list.
## Example
```
add_subscription(1, {"tech", "sports"}, 2)
add_subscription(2, {"tech"}, 1)
news_received(101, "tech")
news_received(102, "sports")
news_received(103, "tech")
publish()
# -> [(1, [101, 102]), (2, [101])]
# Subscriber 1 has three eligible items (101, 102, 103) but limit 2,
# so 103 stays queued. Subscriber 2 has two eligible items (101, 103)
# but limit 1, so 103 stays queued.
publish()
# -> [(1, [103]), (2, [103])]
add_subscription(3, {"sports"}, 5)
publish()
# -> []
# News 102 arrived before subscriber 3's subscription was created,
# so it is not eligible for them; subscribers 1 and 2 have no
# undelivered eligible items left.
news_received(104, "finance")
publish()
# -> []
# No active subscriber is interested in "finance".
```
## Constraints
- Total number of operations across all four types is at most $10^5$.
- `subscriber_id` and `news_id` are positive integers; every `news_id` is unique.
- `topics` contains between 1 and 20 topic strings; topic strings are non-empty lowercase words.
- $1 \le \text{limit} \le 100$.
- `publish` may be called any number of times, including consecutively with no intervening news.
- Aim for an implementation where each operation is efficient enough to handle the full operation stream well within typical time limits; avoid rescanning the entire news store for every subscriber on every `publish` cycle when a subscriber's undelivered backlog can be tracked incrementally.
Quick Answer: This question evaluates a candidate's ability to implement stateful event processing and subscription management, testing competencies in data structures, ordered queues, filtering by attributes, and per-subscriber delivery limits within the coding & algorithms domain.
You are building the core delivery engine for a news publishing service. The service continuously receives news items, stores them, and — whenever a publish cycle is triggered — decides which stored news items should be delivered to which subscribers, based on each subscriber's interests and delivery limits.
Because a coding console runs one function, the whole session is driven by a single `solution(operations)` call. `operations` is a list of command strings applied in order; return a list of strings, one entry for every `publish` command.
**Command grammar** (whitespace-separated):
- `add <subscriber_id> <topic1,topic2,...> <limit>` — register/replace a subscription. `topics` is a comma-joined non-empty set; `limit` is the max items delivered to this subscriber in a single publish cycle. If the subscriber already has an active subscription it is discarded and replaced exactly as if `remove` had been called first: any undelivered backlog is dropped and the new subscription only applies to news that arrives after this call.
- `remove <subscriber_id>` — remove the active subscription; any queued-but-undelivered items are discarded. No-op if the subscriber has no active subscription.
- `news <news_id> <topic>` — a unique news item with a single topic arrives and is stored. The order of `news` commands defines arrival order.
- `publish` — run one delivery cycle.
**Eligibility.** A stored item is eligible for a subscriber when: (1) its topic is in the subscriber's interest set; (2) it arrived AFTER the subscriber's current subscription was created; and (3) it has not already been delivered to this subscriber in an earlier publish cycle.
**A publish cycle.** Each subscriber receives their eligible items in arrival order (oldest first), up to at most `limit` items this cycle. Eligible items beyond the limit are NOT dropped — they stay queued for future cycles. Emit one output string per `publish` command: subscribers that receive at least one item, sorted by `subscriber_id` ascending, joined as `"<sid>:<id,id,...>"` with `";"` between subscribers. A cycle that delivers nothing produces the empty string `""`.
## Example
```
operations = [
"add 1 tech,sports 2",
"add 2 tech 1",
"news 101 tech",
"news 102 sports",
"news 103 tech",
"publish", # subscriber 1 has 101,102,103 eligible but limit 2 -> 101,102 (103 stays); subscriber 2 has 101,103 but limit 1 -> 101
"publish", # remaining backlogs delivered: 1 -> 103, 2 -> 103
"add 3 sports 5",
"publish", # 102 arrived before subscriber 3 subscribed -> ineligible; 1 and 2 have nothing left
"news 104 finance",
"publish" # nobody is interested in finance
]
solution(operations) == ["1:101,102;2:101", "1:103;2:103", "", ""]
```
## Constraints
- Total number of commands is at most 10^5.
- `subscriber_id` and `news_id` are positive integers; every `news_id` is unique.
- A subscription has between 1 and 20 topics; topic strings are non-empty lowercase words.
- 1 <= limit <= 100.
- `publish` may be called any number of times, including consecutively with no intervening news.
- Track each subscriber's undelivered backlog incrementally; avoid rescanning the entire news store for every subscriber on every publish cycle.
Constraints
- Total number of commands is at most 10^5.
- subscriber_id and news_id are positive integers; every news_id is unique.
- A subscription has between 1 and 20 topics; topic strings are non-empty lowercase words.
- 1 <= limit <= 100.
- publish may be called any number of times, including consecutively with no intervening news.
- Track each subscriber's undelivered backlog incrementally; do not rescan the whole news store per publish.
Examples
Input: (["add 1 tech,sports 2", "add 2 tech 1", "news 101 tech", "news 102 sports", "news 103 tech", "publish", "publish", "add 3 sports 5", "publish", "news 104 finance", "publish"],)
Expected Output: ["1:101,102;2:101", "1:103;2:103", "", ""]
Explanation: Official example. Cycle 1: subscriber 1 is eligible for 101,102,103 but limit 2 delivers 101,102 (103 stays queued); subscriber 2 eligible for 101,103 but limit 1 delivers 101. Cycle 2 flushes the leftover 103 to both. Then subscriber 3 subscribes to sports but 102 arrived earlier so is ineligible, and 1 and 2 have nothing left -> empty. finance news interests nobody -> empty.
Input: (["add 1 tech 5", "news 101 tech", "add 1 tech 5", "news 102 tech", "publish"],)
Expected Output: ["1:102"]
Explanation: Replacing subscriber 1's subscription drops the backlog: 101 arrived before the second 'add' so it is discarded, and only 102 (arriving after) is delivered.
Hints
- Only subscribers active at the moment a news item arrives should ever queue it — appending to each interested subscriber's queue on 'news' automatically enforces the 'arrived after subscription created' rule, since later subscribers never see earlier items.
- Keep a per-subscriber FIFO queue (deque) of eligible-but-undelivered news ids. A publish cycle pops up to 'limit' ids from the front; anything left stays for next time, which naturally handles backlog carryover.
- Replacing (add over an existing id) or removing a subscription just drops its queue — there is no separate 'delivered' set to maintain because delivered ids are popped off the queue and never revisited.