Implement Article Vote Tracking
Company: Rippling
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates data structures, state-management, and algorithmic skills for tracking per-user votes, detecting vote flips, maintaining a most-recent unique flip history per user, and computing dynamic top-k article rankings.
Part 1: Track the Last Three Flipped Articles per User
Constraints
- 1 <= len(operations) <= 200000
- Article IDs are assigned incrementally starting from 1 in the order of `add` operations
- Every `article_id` in a vote operation is valid
- 0 <= user_id <= 10^9
- Article names are non-empty strings
- If a user has fewer than 3 flipped articles, return all of them
- If a user has never flipped on any article, return an empty list for that query
Examples
Input: ([('add_article', 'Alpha'), ('add_article', 'Beta'), ('upvote', 'u1', 1), ('downvote', 'u1', 1), ('upvote', 'u1', 2), ('downvote', 'u1', 2), ('last_three_flips', 'u1')],)
Expected Output: [['Beta', 'Alpha']]
Explanation: Alpha is flipped first, then Beta is flipped later, so Beta is returned before Alpha.
Input: ([('add_article', 'Alpha'), ('add_article', 'Beta'), ('upvote', 'u1', 1), ('downvote', 'u1', 1), ('upvote', 'u1', 2), ('downvote', 'u1', 2), ('upvote', 'u1', 1), ('last_three_flips', 'u1')],)
Expected Output: [['Alpha', 'Beta']]
Explanation: Alpha flips, then Beta flips, then Alpha flips again. Alpha should move to the front and appear only once.
Input: ([('add_article', 'Alpha'), ('upvote', 'u1', 1), ('upvote', 'u1', 1), ('last_three_flips', 'u1'), ('last_three_flips', 'u2')],)
Expected Output: [[], []]
Explanation: The second upvote repeats the same vote, so no flip is recorded. A user with no flips returns an empty list.
Input: ([('add_article', 'A'), ('add_article', 'B'), ('add_article', 'C'), ('add_article', 'D'), ('upvote', 'u1', 1), ('downvote', 'u1', 1), ('upvote', 'u1', 2), ('downvote', 'u1', 2), ('upvote', 'u1', 3), ('downvote', 'u1', 3), ('upvote', 'u1', 4), ('downvote', 'u1', 4), ('last_three_flips', 'u1')],)
Expected Output: [['D', 'C', 'B']]
Explanation: Four different articles are flipped, but only the three most recent unique flipped articles are returned.
Input: ([('add_article', 'Alpha'), ('add_article', 'Beta'), ('add_article', 'Gamma'), ('upvote', 'u1', 1), ('downvote', 'u1', 1), ('downvote', 'u2', 2), ('upvote', 'u2', 2), ('upvote', 'u1', 3), ('downvote', 'u1', 3), ('last_three_flips', 'u2'), ('last_three_flips', 'u1')],)
Expected Output: [['Beta'], ['Gamma', 'Alpha']]
Explanation: Each user has an independent vote history and flip order.
Input: ([],)
Expected Output: []
Explanation: With no operations, there are no query answers to return.
Hints
- Track the latest vote for each `(article_id, user_id)` pair so you can detect whether a new vote is a real flip.
- For each user, maintain an ordered set/map of flipped article IDs. On a new flip, move that article to the most recent position instead of storing duplicates.
Part 2: Return the Top K Articles by Current Score
Constraints
- 1 <= len(operations) <= 200000
- 0 <= sum of all k values across `top_k` queries <= 200000
- Article IDs are assigned incrementally starting from 1 in the order of `add` operations
- Every `article_id` in a vote operation is valid
- 0 <= user_id <= 10^9
- 0 <= k <= number of added articles is allowed, but if k is larger, return all available articles
- Tie-break equal scores by smaller article ID first
Examples
Input: [('add', 'Alpha'), ('add', 'Beta'), ('upvote', 'alice', 1), ('top_k', 2)]
Expected Output: [['Alpha', 'Beta']]
Explanation: Alpha has score 1 and Beta has score 0, so Alpha comes first.
Input: [('add', 'Alpha'), ('add', 'Beta'), ('upvote', 'alice', 2), ('upvote', 'bob', 2), ('upvote', 'cara', 1), ('top_k', 2)]
Expected Output: [['Beta', 'Alpha']]
Explanation: Beta has score 2 and Alpha has score 1, so Beta ranks higher.
Input: [('add', 'Gamma'), ('add', 'Delta'), ('upvote', 'u1', 1), ('upvote', 'u1', 1), ('downvote', 'u1', 1), ('upvote', 'u2', 2), ('top_k', 2)]
Expected Output: [['Delta', 'Gamma']]
Explanation: Gamma goes from 0 to 1, repeated upvote changes nothing, then flips to -1. Delta gets +1, so Delta ranks above Gamma.
Input: [('add', 'First'), ('add', 'Second'), ('upvote', 'u1', 2), ('upvote', 'u2', 1), ('top_k', 2)]
Expected Output: [['First', 'Second']]
Explanation: Both articles have score 1, so the smaller article ID wins the tie. First was added earlier.
Input: [('top_k', 3), ('add', 'Solo'), ('top_k', 5)]
Expected Output: [[], ['Solo']]
Explanation: Before any article exists, the answer is an empty list. Later, only Solo exists, so it is returned even though k is 5.
Input: [('add', 'Alpha'), ('add', 'Beta'), ('upvote', 'alice', 1), ('top_k', 2), ('upvote', 'bob', 2), ('upvote', 'cara', 2), ('top_k', 2)]
Expected Output: [['Alpha', 'Beta'], ['Beta', 'Alpha']]
Explanation: The function must return answers for all top_k queries in order. After the later votes, Beta overtakes Alpha.
Hints
- Store the latest vote for each `(article_id, user_id)` pair and update the article score by `new_vote - old_vote` whenever the vote changes.
- To answer many `top_k` queries efficiently, maintain a heap of candidate scores and use lazy deletion (for example, with version numbers) to ignore stale entries.