Determine Redeemable Promotion Offers from Event History
Company: Affirm
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
## Problem
Determine which promotion offers each user may redeem at a cutoff time. Offers have an ID, an inclusive start time, an exclusive end time, and a per-user limit on active redemptions. Events record a user redeeming or unredeeming an offer. Apply all events at or before the cutoff, then return every event user's currently redeemable offers.
### Function Contract
Implement `redeemable_offers(offer_ids, start_times, end_times, limits, event_users, event_offer_ids, event_times, event_actions, cutoff) -> list[list[str]]`.
The four offer arrays are parallel: index `i` describes offer `offer_ids[i]`. The four event arrays are also parallel: index `j` describes one event. Times and `cutoff` are nonnegative integer UTC seconds. Each action is exactly `"redeem"` or `"unredeem"`.
Return one row per distinct user appearing in `event_users`, sorted by user ID. A row begins with the user ID, followed by every offer that is active at the cutoff and whose active redemption count for that user is below its limit. Sort those offer IDs lexicographically. A user with no eligible offer is represented by a one-element row containing only the user ID.
### Constraints
- `1 <= len(offer_ids) <= 2000`; offer IDs are unique nonempty ASCII strings of at most 32 characters.
- `len(start_times) = len(end_times) = len(limits) = len(offer_ids)`.
- `0 <= start_times[i] < end_times[i] <= 9007199254740991`.
- `1 <= limits[i] <= 10^9`.
- `0 <= len(event_users) <= 200000`, and all four event arrays have that length.
- User IDs are nonempty ASCII strings of at most 32 characters; every event offer ID appears in `offer_ids`.
- `0 <= event_times[j], cutoff <= 9007199254740991`.
- Events need not be sorted. Process events with `event_times[j] <= cutoff` by `(event_times[j], j)`.
- Valid input never lets an active redemption count become negative or exceed its offer limit.
- The number of distinct event users multiplied by the number of offers is at most 200000.
- Offer `i` is active exactly when `start_times[i] <= cutoff < end_times[i]`.
### Examples
- Offers `["A","B","C"]` have starts `[0,0,50]`, ends `[100,40,100]`, and limits `[2,1,1]`. Events have users `["u1","u1","u2","u2"]`, offer IDs `["A","A","A","B"]`, times `[10,20,5,10]`, and actions `["redeem","unredeem","redeem","redeem"]`. At cutoff `60`, return `[["u1","A","C"],["u2","A","C"]]`.
- An offer whose end time equals the cutoff is not returned.
```hint Aggregate by user and offer
Sort the relevant event indices by time and original position, then maintain a nested active-count map.
```
### Edge Cases
- A user can unredeem and later redeem again.
- Events after the cutoff have no effect.
- Expired offers can remain in the count map but are not returned.
- A user with no eligible offer still appears in the output.
Quick Answer: Determine each event user's redeemable promotions at a cutoff by replaying ordered redeem and unredeem events, applying half-open offer windows and per-user limits, and sorting results deterministically.
Determine which promotion offers each event user may redeem at cutoff. Parallel offer arrays provide each unique offer ID, inclusive start time, exclusive end time, and per-user active-redemption limit. Parallel event arrays provide a user, offer ID, integer UTC time, and an action equal to redeem or unredeem. Apply events at or before cutoff in increasing (event time, original index) order. Return one row for every distinct user appearing in event_users, sorted by user ID. Each row begins with that user ID, followed by all offers active at cutoff whose current count for the user is below the offer limit, sorted lexicographically. A user with no eligible offer still has a one-element row.
Constraints
- 1 <= len(offer_ids) <= 2000, and the four offer arrays have equal length.
- 0 <= len(event_users) <= 200000, and the four event arrays have equal length.
- Offer and user IDs are nonempty ASCII strings of at most 32 characters; offer IDs are unique.
- 0 <= times and cutoff <= 9007199254740991; starts are strictly before ends.
- 1 <= limits[i] <= 1000000000, and valid input never makes a replayed count negative or above its limit.
- The number of distinct event users multiplied by the number of offers is at most 200000.
Examples
Input: (['A', 'B', 'C'], [0, 0, 50], [100, 40, 100], [2, 1, 1], ['u1', 'u1', 'u2', 'u2'], ['A', 'A', 'A', 'B'], [10, 20, 5, 10], ['redeem', 'unredeem', 'redeem', 'redeem'], 60)
Expected Output: [['u1', 'A', 'C'], ['u2', 'A', 'C']]
Explanation: Expired B is omitted, while both users remain below A's limit and have no C redemption.
Input: (['A'], [0], [10], [1], [], [], [], [], 5)
Expected Output: []
Explanation: With no event users there are no output rows.
Hints
- Replay relevant event indices in (time, original index) order into counts keyed by user and offer.
- Treat the start boundary as inclusive and the end boundary as exclusive.