Coding questions · Databricks
Design Tic-Tac-Toe and QPS data structures
This question evaluates data-structure and API design skills, covering state management and efficient update/query algorithms for a generalized Tic-Tac-Toe with uniform-random move selection, as well as time-series aggregation and streaming-log techniques for computing QPS from non-decreasing timestamps.
You are given two independent coding problems that focus on data structure and API design.
Problem 1: Generalized Tic-Tac-Toe Game with Simple AI
Design a Tic-Tac-Toe game that supports a generalized board size and a customizable win condition.
Implement a class TicTacToe with the following behavior:
- The game is played by two players, labeled
1and2. - The board has
rowsrows andcolscolumns. - A player wins if they have K of their marks in a line, where a line can be:
- A horizontal line
- A vertical line
- A main diagonal (top-left to bottom-right)
- An anti-diagonal (top-right to bottom-left)
API
TicTacToe(int rows, int cols, int K)
- Initializes the game board with the given dimensions and win condition
K. - All cells are initially empty.
int move(int row, int col, int player)
row,colare 0-indexed coordinates on the board.playeris either1or2.- Places the player's mark at
(row, col). - Returns:
0if no one has won after this move,1if player 1 wins as a result of this move,2if player 2 wins as a result of this move,-1if the move is invalid (out of bounds or the cell is already occupied).
You should design the data structures so that each move call is efficient even when the board is large.
Constraints (you may assume):
1 <= rows, cols <= 10001 <= K <= max(rows, cols)- Total number of moves ≤
rows * cols.
Follow-up: Random-Move AI
Extend the design with a very simple AI that chooses a random legal move for a given player.
You are given a helper function:
int randInt(int low, int high)
which returns a uniform random integer in the inclusive range [low, high].
Add a method:
pair<int, int> getNextMove(int player)
- Returns a pair
(row, col)corresponding to an empty cell whereplayercan play next. - The AI should pick uniformly at random among all currently empty cells.
- If there is no legal move (the board is full or the game is already over), you may return a special value, such as
(-1, -1).
Design this so that getNextMove runs efficiently, even on a large board with many moves already played.
Problem 2: Query QPS from KV Store Request Logs
A key-value (KV) store receives a large number of requests. Each request is logged with a timestamp (in seconds). You want to support efficient queries about the queries per second (QPS) over arbitrary time ranges.
You are given an initially empty log. Implement a data structure that supports the following operations:
void record(long timestamp)
- Called once for each request handled by the KV store.
timestampis an integer representing seconds since some fixed epoch.- Timestamps are not guaranteed to be contiguous but you may assume they are non-decreasing (each new call has
timestamp >=previoustimestamp).
double getQPS(long startTime, long endTime)
-
Returns the average QPS in the inclusive time interval
[startTime, endTime]. -
Formally, if
countrequests have timestampstsuch thatstartTime <= t <= endTime, then:QPS = count / (endTime - startTime + 1) -
You should support many
getQPSqueries efficiently after recording a large volume of data.
Constraints (you may assume):
- Up to
N = 10^6calls torecord. - Up to
Q = 10^6calls togetQPS. - Timestamps fit in a 64-bit signed integer.
Follow-up 1: Efficient Arbitrary-Time Queries
The naive implementation might scan all timestamps within [startTime, endTime] for each query, which is too slow when Q is large.
Design and describe a more efficient approach that:
- Uses reasonable additional memory.
- Supports
getQPS(startTime, endTime)in sublinear time inN(for example,O(log N)per query).
Your design should be implementable using common data structures (arrays, lists, hash maps, trees, etc.).
Follow-up 2: Trade Accuracy for Less Memory
Now suppose memory is tight and you are allowed to sacrifice precision to save space. Instead of storing every individual timestamp, you can approximate the QPS.
Design a modified scheme that:
- Uses significantly less memory than the exact solution.
- Returns an approximate QPS for any
[startTime, endTime]query. - Clearly states what is being approximated (for example, grouping multiple seconds into a time range / bucket) and what kind of error might be introduced.
You should:
- Describe the data you would store (e.g., buckets or ranges instead of individual timestamps).
- Explain how
getQPSwill be computed from this aggregated data. - Reason about the trade-off between memory usage, accuracy, and query performance.
Part 1: Generalized Tic-Tac-Toe Move Engine
Implement the core move engine for a generalized Tic-Tac-Toe game played on an arbitrary board with a configurable winning run length.
Task
Write a function solution(rows, cols, k, moves) that replays a sequence of move attempts on an empty rows x cols board and returns the result of each attempt.
Inputs
rows,cols— the board dimensions. Cells are addressed by 0-indexed coordinates: valid rows are0 .. rows - 1and valid columns are0 .. cols - 1.k— the number of consecutive marks a player must line up to win.moves— a list of move attempts processed in order. Each attempt is a triple(row, col, player), whereplayeris intended to be1or2.
Output
Return a list of integers, one per move attempt, in the same order as moves. For each attempt, append:
0— the move is valid and placed, but no one has won yet.1or2— the move is valid and causes that player to win on this move.-1— the move is invalid (see below) and is not placed on the board.
Rules
Process each attempt (row, col, player) one at a time:
-
Validate the move. The result is
-1if any of these holds:- the game has already ended (a previous move won), or
playeris not1or2, or- the cell is out of bounds (i.e.
roworcolfalls outside the board), or - the cell is already occupied by an earlier mark.
An invalid move leaves the board unchanged and the game state untouched.
-
Place the mark. For a valid move, record
player's mark at(row, col). -
Check for a win. The player wins if placing this mark forms a run of at least
kconsecutive marks of theirs along any of the four axes through the just-placed cell:- horizontal (left–right),
- vertical (up–down),
- diagonal, or
- anti-diagonal.
The run is counted in both directions from the new cell. If the run reaches length
kon any axis, the player wins and the game ends (all later attempts then return-1). Otherwise the result is0.
Notes
- You do not need to enforce alternating turns — the same player may move twice in a row, and an out-of-turn move is still valid as long as it passes the checks above.
- Once a player has won, the game is over: every remaining attempt yields
-1.
Constraints
1 <= rows, cols <= 10001 <= k <= max(rows, cols)0 <= len(moves) <= rows * cols- Each
playervalue is intended to be1or2; any other value makes that move invalid (-1).
Examples
Example 1
Input: (3, 3, 3, [(0, 0, 1), (1, 0, 2), (0, 1, 1), (1, 1, 2), (0, 2, 1)])
Output: [0, 0, 0, 0, 1]
Explanation: Player 1 wins horizontally on the top row with the last move.
Example 2
Input: (3, 4, 3, [(0, 1, 2), (0, 0, 1), (1, 1, 1), (2, 2, 1)])
Output: [0, 0, 0, 1]
Explanation: Player 1 forms a main diagonal of length 3.
Hints
- Only the most recent move can create a new win, so you never need to rescan the whole board.
- For each move, check four direction pairs: horizontal, vertical, main diagonal, and anti-diagonal.
Approach
The engine processes each move attempt in order, tracking placed marks in a board dict keyed by (row, col), a results list, and a game_over flag.
Validation. A move returns -1 if any of these hold: the game already ended (game_over), the player isn't 1 or 2, the cell is out of bounds (0 <= row < rows, 0 <= col < cols), or the cell is already occupied ((row, col) in board). These checks are short-circuited in one if, so an invalid move never touches the board.
Placement + win check. For a valid move, the mark is stored, then the code tests the four win axes via directions = ((1,0), (0,1), (1,1), (1,-1)) — vertical, horizontal, diagonal, anti-diagonal. For each axis it counts the run through the just-placed cell:
- Start
count = 1(the new mark). - Walk in the
(dr, dc)direction, incrementing while the neighbour is in bounds andboard.get((r,c)) == player. - Walk the opposite
(-dr, -dc)direction the same way.
Each while is bounded by count < k, so it stops as soon as k is reached — no need to scan further. If any axis yields count >= k, the player wins: game_over is set and player is appended. Otherwise 0 is appended.
Why it's correct. Only the newly placed cell can complete a line, so checking the maximal run through it on all four axes is sufficient and necessary. Using a dict (rather than a full rows*cols grid) keeps memory proportional to actual moves, which matters since the board can be up to 1000×1000.
- Time complexity: O(M * k)
- Space complexity: O(M)
Part 2: Random Legal Move Selector for Generalized Tic-Tac-Toe
Implement solution(rows, cols, k, operations) for generalized Tic-Tac-Toe on a rows × cols board, where a player wins by getting k of their marks in a row. This part adds a deterministic "AI move suggestion" query on top of the normal move logic.
You process the operations list in order and return a list of results, one entry per operation.
Board and Players
- The board has
rowsrows andcolscolumns; cells are indexed by(row, col)with0 <= row < rowsand0 <= col < cols. - There are two players, identified by the integers
1and2. - A player wins immediately when, after placing a mark, they have
kconsecutive marks along any of the four axes: horizontal, vertical, or either diagonal. Once a player wins, the game is over and every subsequent operation is rejected.
Operations
Each operation is a tuple. There are two kinds:
('move', row, col, player)
Attempt to place player's mark at cell (row, col).
- The move is rejected if any of the following holds — append
-1:- the game is already over,
playeris not1or2,(row, col)is out of bounds,- the cell
(row, col)is already occupied.
- Otherwise the mark is placed, and:
- if this move makes
playerwin (reacheskin a row), appendplayerand mark the game as over; - otherwise append
0.
- if this move makes
('ai', player, rank)
Ask for a candidate empty cell, without placing any mark (board state is unchanged).
- Let
Ebe the number of currently empty cells. Conceptually, the AI picks uniformly at random among empty cells;rankis supplied as the index of that draw so the result is deterministic and testable. - If the query is valid, return the
rank-th empty cell in row-major order among the currently empty cells, as a(row, col)pair. Row-major order means cells are ordered byrowfirst, thencol.rankis 0-indexed (sorank = 0is the first empty cell). - The query fails — append
(-1, -1)— if any of the following holds:- the game is already over,
playeris not1or2,- there are no empty cells (
E == 0), rankis out of range, i.e. not0 <= rank < E.
Any other operation type
If an operation's type is neither 'move' nor 'ai', append -1 for it.
Return Value
Return the list of per-operation results described above:
- a
moveyieldsplayer,0, or-1; - an
aiquery yields a(row, col)pair or(-1, -1).
Constraints
1 <= rows, cols1 <= rows * cols <= 2000001 <= k <= max(rows, cols)0 <= len(operations) <= 200000
Examples
Example 1
Input: (2, 2, 2, [('ai', 1, 2), ('move', 0, 1, 1), ('ai', 2, 1)])
Output: [(1, 0), 0, (1, 0)]
Explanation: Initially the 3rd empty cell in row-major order is (1, 0). After occupying (0, 1), the 2nd empty cell is still (1, 0).
Example 2
Input: (1, 3, 1, [('ai', 1, 1), ('move', 0, 2, 2), ('ai', 1, 0), ('move', 0, 1, 1)])
Output: [(0, 1), 2, (-1, -1), -1]
Explanation: The move by player 2 wins immediately because k = 1, so later AI and move operations are invalid.
Hints
- If
ranktells you which empty cell to choose, the AI query becomes a k-th available-position query. - A Fenwick tree or segment tree can track which cells are still empty in row-major order.
Approach
Approach. The hard part is the ('ai', player, rank) query: return the rank-th currently empty cell in row-major order, fast. A linear scan would be O(rows·cols) per query, too slow at 200k operations. Instead the code keeps a Fenwick tree (BIT) over the flattened cells 0…rows·cols−1, where each position holds 1 while empty and 0 once filled. The BIT then answers "find the index whose prefix-sum equals rank+1" in O(log n).
Key pieces:
Fenwick.__init__seeds every cell to 1 using the O(n) trickbit[i] = i & -i, so all cells start empty.kth(k)does a binary-lifting descent: starting from the highest power of two ≥n, it greedily moves right whenever the running prefix sum stays belowk, landing on the smallest index whose prefix sum isk. That index (1-based, minus 1) is the flat position of thek-th empty cell.- A
moveop validates (game live, player ∈ {1,2}, in-bounds, cell empty), records it inboard, sets that cell's BIT entry to 0 viaadd(..., -1), decrementstotal_empty, then callscheck_win. check_winwalks both directions of each of the 4 axes (|,—,\,/) from the new mark, counting consecutive same-player cells; reachingkis a win and freezes the game.- An
aiop validatesgame_over, player,total_empty > 0, and0 ≤ rank < total_empty; on failure returns(-1, -1). Otherwiseidx = kth(rank+1) - 1and it returns(idx // cols, idx % cols). It never mutates state.
Correctness. Because the BIT prefix sum at index i equals the number of empty cells at positions ≤ i, kth returns exactly the row-major rank-th empty cell, matching the uniform-draw semantics deterministically.
- Time complexity: O((M + Q)·log(rows·cols) + M·k) where M = number of move ops, Q = number of ai ops. Each Fenwick update/query is O(log(rows·cols)); each move additionally runs check_win in O(k). Initial Fenwick build is O(rows·cols).
- Space complexity: O(rows·cols) for the Fenwick array, plus O(P) for the board dict where P is the number of played cells (P ≤ rows·cols), so O(rows·cols) overall.
Part 3: Online Exact QPS Tracker
Process a stream of interleaved log and query operations for a key-value store and return the answer to each query.
Implement:
def solution(operations):
...
Input
operations is a list where each entry is one of two operations:
('record', timestamp)— log one request that arrived at integertimestamp. Record timestamps appear in non-decreasing order across the stream.('getQPS', startTime, endTime)— query the average QPS (queries per second) over the inclusive time window[startTime, endTime], using only the requests recorded so far (i.e. by operations that appear before this query in the list).
The two kinds of operations are interleaved in any order.
Output
Return a list of floats containing one answer for each getQPS operation only, in the order those queries appear. record operations produce no output.
How to compute a query answer
For a getQPS(startTime, endTime) query:
- Let
count= the number of requests recorded so far whosetimestamplies in the inclusive interval[startTime, endTime]. - The interval spans
endTime - startTime + 1seconds (inclusive of both endpoints). - The average QPS is
count / (endTime - startTime + 1).
Multiple requests may share the same timestamp; each one counts individually.
Edge cases
- If
startTime > endTime, the answer for that query is0.0. - If no requests have been recorded yet (or no recorded request falls inside the window), the answer is
0.0.
Examples
Example 1
operations = [('record', 1), ('record', 1), ('record', 3), ('getQPS', 1, 3), ('getQPS', 2, 2)]
returns [1.0, 0.0]
getQPS(1, 3): 3 requests in[1, 3]over a 3-second span →3 / 3 = 1.0.getQPS(2, 2): no request at timestamp 2 →0 / 1 = 0.0.
Example 2
operations = [('record', 10), ('record', 13), ('record', 13), ('getQPS', 10, 13), ('getQPS', 11, 12)]
returns [0.75, 0.0]
getQPS(10, 13): 3 requests in[10, 13]over a 4-second span →3 / 4 = 0.75.getQPS(11, 12): no request in[11, 12]→0.0.
Example 3
operations = [('getQPS', 5, 7), ('record', 6), ('getQPS', 6, 6)]
returns [0.0, 1.0]
getQPS(5, 7): nothing recorded yet →0.0.getQPS(6, 6): 1 request at timestamp 6 over a 1-second span →1.0.
Example 4
operations = [('record', 2), ('getQPS', 5, 3), ('getQPS', 0, 2)]
returns [0.0, 0.3333333333333333]
getQPS(5, 3):startTime > endTime→0.0.getQPS(0, 2): 1 request in[0, 2]over a 3-second span →1 / 3 = 0.3333333333333333.
Constraints
- Up to
10^6operations. - Record timestamps are non-decreasing.
- Timestamps fit in a signed 64-bit integer.
Hints
- Many consecutive
recordcalls may share the same timestamp, so storing one count per distinct time can save space. - Use binary search on the distinct timestamps to count how many requests fall inside a query range.
Part 4: Fast Arbitrary-Range QPS Queries from Sorted Logs
Answer many average-QPS (queries-per-second) range queries over a static, sorted request log — each query in sublinear time, without scanning the whole log.
What to implement
solution(timestamps, queries)
timestamps— a list of request times, sorted in non-decreasing order (duplicates allowed). This log is fixed; it never changes between queries.queries— a list of(startTime, endTime)pairs.
Return a list of floats, one per query, in the same order as the input queries.
What each query asks
For a query (startTime, endTime), compute the average QPS over the inclusive interval [startTime, endTime], defined as:
(number of timestamps t with startTime <= t <= endTime)
-----------------------------------------------------------
endTime - startTime + 1
- The numerator is how many log entries fall inside the closed interval — both endpoints are included, and duplicate timestamps each count.
- The denominator is the number of integer time units the interval spans. The
+ 1makes the interval inclusive of both endpoints (for example,[5, 5]spans 1 unit, not 0).
Example: with three entries at time 5, the query (5, 5) returns 3 / (5 - 5 + 1) = 3.0.
Rules and edge cases
- Inverted range: if
startTime > endTime, return0.0for that query. - Empty log: if
timestampsis empty, every query returns0.0. - No matches: if no timestamp falls in
[startTime, endTime], the numerator is0, so the result is0.0. - Negative times are valid. Timestamps and query bounds may be negative; only the ordering matters.
- Each query is independent — answering one does not affect any other.
Constraints
0 <= len(timestamps) <= 10^60 <= len(queries) <= 10^6timestampsis sorted in non-decreasing order.- Every timestamp fits in a signed 64-bit integer.
Because the log is pre-sorted, aim to answer each query in O(log N) rather than scanning all N timestamps.
Hints
- In a sorted array, the number of values inside a range can be found with two binary searches.
- You only need the first index >= startTime and the first index > endTime.
Approach
Goal. For each query (start, end), compute the average QPS over the inclusive interval [start, end] — that is, (number of requests whose timestamp falls in the interval) ÷ (number of integer time units in the interval).
Key idea — binary search instead of scanning. The log is already sorted non-decreasing, so we never need to walk it. For each query we run two binary searches over timestamps:
left = bisect_left(timestamps, start)— the index of the first timestamp>= start.right = bisect_right(timestamps, end)— the index one past the last timestamp<= end.
Then right - left is exactly the count of timestamps lying in [start, end] (a half-open [left, right) slice). bisect_left/bisect_right correctly handle duplicate timestamps: ties on start are included (left bound) and ties on end are included (right bound), so a closed interval is counted exactly.
The denominator. Average QPS divides that count by the span end - start + 1. The +1 makes the interval inclusive of both endpoints (e.g. [5,5] spans 1 unit, not 0), which matches the test where ([5,5,5], (5,5)) -> 3.0.
Edge cases. If start > end or the log is empty, we append 0.0 and skip — this guards the empty-log case and the inverted-range case (e.g. query (3,1) -> 0.0). Negative timestamps work unchanged because bisect only relies on ordering.
Why it's correct & fast. Each query is independent and answered with two O(log N) searches, so we avoid any per-query linear scan over the up-to-10^6 timestamps.
- Time complexity: O(Q log N)
- Space complexity: O(1) extra space, excluding the O(Q) output list
Part 5: Approximate QPS with Fixed-Width Time Buckets
Estimate the approximate QPS (queries per second) over time windows using a low-memory bucketed counting scheme — without storing every individual request timestamp.
Implement:
def solution(bucket_size, timestamps, queries):
...
Idea
Instead of keeping every timestamp, group the timeline into fixed-width buckets of bucket_size consecutive seconds and store only the request count per bucket. A timestamp t belongs to bucket b = t // bucket_size (floor division), so bucket b covers the inclusive integer-second range:
[b * bucket_size, b * bucket_size + bucket_size - 1]
To answer a query, assume requests are uniformly distributed within each bucket, so a bucket contributes a fraction of its count proportional to how much of it the query window covers.
Input
bucket_size(int): the width of each bucket, in seconds.timestamps(list of ints): the second at which each request occurred. Timestamps may repeat and may be negative; floor division still applies.queries(list of[startTime, endTime]pairs): each query asks for the approximate QPS over the inclusive time window[startTime, endTime](both endpoints included).
Output
Return a list of floats, one per query (in the same order), where each value is the approximate QPS for that query's window.
How to answer one query [start, end]
The window spans duration = end - start + 1 inclusive seconds.
-
Approximate request count. Sum, over every bucket that the window touches:
bucket_count * (overlap_len / bucket_size)where
overlap_lenis the inclusive integer overlap between the window[start, end]and the bucket's range, i.e.overlap_len = max(0, min(end, bucket_end) - max(start, bucket_start) + 1)- A bucket fully inside the window has
overlap_len == bucket_size, so it contributes its entire count. - The two boundary buckets (those containing
startandend) are typically only partially overlapped, so they contribute a prorated fraction of their count.
- A bucket fully inside the window has
-
Approximate QPS. Divide the approximate request count by the window length:
qps = approx_count / duration
Rules / edge cases
- Invalid window: if
startTime > endTime, the result for that query is0.0. - An empty
timestampslist (or a window touching no requests) yields0.0for the affected queries. - Negative timestamps and negative query endpoints are valid and handled by floor division.
Examples
bucket_size = 10,timestamps = [0, 1, 9, 10, 11, 19],queries = [[0, 9], [5, 14]]→[0.3, 0.3].bucket_size = 4,timestamps = [0, 0, 0, 3, 4, 7],queries = [[1, 4]]→[0.875]. Bucket0 = [0,3]has count4and overlaps[1,3](length3), contributing4 * 3/4 = 3; bucket1 = [4,7]has count2and overlaps[4,4](length1), contributing2 * 1/4 = 0.5; total3.5, divided byduration = 4, gives0.875.bucket_size = 5,timestamps = [],queries = [[0, 4], [3, 2]]→[0.0, 0.0](no data; second query is invalid).bucket_size = 3,timestamps = [-3, -2, 0, 2, 3],queries = [[-3, -1], [0, 5]]→[0.6666666666666666, 0.5](negative timestamps via floor division).
Constraints
1 <= bucket_size <= 10^90 <= len(timestamps) <= 2000000 <= len(queries) <= 200000- Timestamps fit in a signed 64-bit integer.
Hints
- Map each timestamp
tto buckett // bucket_sizeand count how many records fall in each bucket. - For a partial bucket, multiply that bucket's count by
overlap_length / bucket_size.
Approach
Idea. To avoid storing every timestamp, the algorithm buckets time into fixed windows of bucket_size seconds and keeps only a per-bucket request count. A query is answered by assuming requests are spread uniformly inside each bucket, so a bucket that is partially overlapped contributes a fraction of its count proportional to the overlap.
Build phase.
bucket_counts[b]counts timestamps falling in bucketb = t // bucket_size.- The non-empty bucket ids are sorted, and a
prefixarray of cumulative counts is built over them. This lets us sum counts across any contiguous range of buckets inO(log U).
Query phase. For each [start, end]:
- If
start > end, return0.0. duration = end - start + 1(inclusive seconds),start_bucket/end_bucketvia integer division.- Same bucket (
start_bucket == end_bucket): the whole window lies in one bucket, so it contributescount * (duration / bucket_size). - Spanning buckets: the two boundary buckets contribute
count * (overlap_len / bucket_size), whereoverlap_lenis the inclusive integer overlap of the window with[bucket*bucket_size, bucket*bucket_size + bucket_size - 1]. All interior buckets are fully covered, sobucket_sum(start_bucket+1, end_bucket-1)adds their entire counts via prefix sums +bisect. - QPS =
approx_count / duration.
Why correct. Uniform-distribution assumption makes a bucket's contribution linear in overlap length, so summing whole interior counts plus prorated boundary counts gives the approximate request total; dividing by duration yields per-second rate. Negative timestamps work because floor division and prefix lookups are sign-agnostic. The empty-timestamp and start > end cases are handled explicitly.
- Time complexity: Preprocessing: O(N + U log U) (N = number of timestamps, U = number of non-empty buckets). Per query: O(log U) from two binary searches over the sorted bucket ids; total query cost O(Q log U).
- Space complexity: O(U) for the bucket-count dict, the sorted bucket-id list, and the prefix-sum array (U = number of distinct non-empty buckets, at most N).