Implement task queue with insert, delete, execute
Company: Citadel
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Onsite
Quick Answer: This question evaluates a candidate's ability to design and implement efficient dynamic priority management, including priority ordering, insertion-order tie-breaking, and support for add, remove, and execute operations.
Part 1: Simulate the 2048 Board
Constraints
- 0 <= n <= 20
- 0 <= len(moves) <= 10^4
- Each board value is 0 or a positive power of 2
- The board is square
Examples
Input: ([[2, 0, 2, 2], [4, 4, 0, 4], [0, 0, 2, 2], [2, 2, 2, 2]], 'L')
Expected Output: [[4, 2, 0, 0], [8, 4, 0, 0], [4, 0, 0, 0], [4, 4, 0, 0]]
Explanation: Each row is compressed left, and pairs merge only once per move.
Input: ([[2, 0], [2, 2]], 'U')
Expected Output: [[4, 2], [0, 0]]
Explanation: The first column merges into 4; the second column becomes [2, 0].
Input: ([], 'LR')
Expected Output: []
Explanation: Edge case: empty board.
Input: ([[2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 'L')
Expected Output: [[4, 4, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
Explanation: A row [2,2,2,2] becomes [4,4,0,0], not [8,0,0,0].
Solution
def solution(board, moves):
if not board:
return []
n = len(board)
def compress(line):
vals = [x for x in line if x != 0]
merged = []
i = 0
while i < len(vals):
if i + 1 < len(vals) and vals[i] == vals[i + 1]:
merged.append(vals[i] * 2)
i += 2
else:
merged.append(vals[i])
i += 1
merged.extend([0] * (n - len(merged)))
return merged
for mv in moves:
if mv == 'L':
board = [compress(row) for row in board]
elif mv == 'R':
board = [list(reversed(compress(list(reversed(row))))) for row in board]
elif mv == 'U':
cols = []
for c in range(n):
col = compress([board[r][c] for r in range(n)])
cols.append(col)
board = [[cols[c][r] for c in range(n)] for r in range(n)]
elif mv == 'D':
cols = []
for c in range(n):
original = [board[r][c] for r in range(n)]
col = list(reversed(compress(list(reversed(original)))))
cols.append(col)
board = [[cols[c][r] for c in range(n)] for r in range(n)]
return boardTime complexity: O(n^2 * m), where m = len(moves). Space complexity: O(n^2).
Hints
- Solve one row or one column at a time: remove zeros, merge adjacent equal values once, then pad with zeros.
- You only need one merge routine. For right/down moves, reverse first and reverse back after merging.
Part 2: Encode and Decode a 2048 Board into 64 Bits
Constraints
- The board size is always 4 x 4
- Each board value is either 0 or a power of 2 up to 32768
- The encoded result fits in an unsigned 64-bit integer
Examples
Input: ('encode', [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
Expected Output: 0
Explanation: All zero exponents produce integer 0.
Input: ('encode', [[2, 4, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
Expected Output: 33
Explanation: Exponents are [1, 2, 0, ...], so code = 1 + (2 << 4) = 33.
Input: ('decode', 4369)
Expected Output: [[2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
Explanation: 4369 = 0x1111, so the first four cells have exponent 1.
Input: ('decode', 15)
Expected Output: [[32768, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
Explanation: The lowest nibble is 15, which decodes to 2^15 = 32768.
Solution
def solution(mode, data):
if mode == 'encode':
board = data
code = 0
idx = 0
for row in board:
for val in row:
if val == 0:
exp = 0
else:
exp = val.bit_length() - 1
code |= exp << (4 * idx)
idx += 1
return code
if mode == 'decode':
code = data
board = [[0] * 4 for _ in range(4)]
for idx in range(16):
exp = (code >> (4 * idx)) & 15
board[idx // 4][idx % 4] = 0 if exp == 0 else (1 << exp)
return board
raise ValueError('mode must be either encode or decode')Time complexity: O(1). Space complexity: O(1).
Hints
- A 2048 tile is fully determined by its exponent, so 4 bits per cell are enough here.
- For cell index i in row-major order, use bit positions [4*i, 4*i+3].
Part 3: Dynamic Weighted Sampling with Insert/Delete
Constraints
- 1 <= n <= 2 * 10^5
- 1 <= number of operations <= 2 * 10^5
- 1 <= weight <= 10^9
- Sample queries are always valid: 1 <= r <= current total weight
Examples
Input: (5, [('insert', 1, 3), ('insert', 3, 2), ('sample', 1), ('sample', 4), ('delete', 1), ('sample', 2)])
Expected Output: [1, 3, 3]
Explanation: The sample queries hit the prefix ranges for IDs 1, then 3, then 3 again after deleting ID 1.
Input: (4, [('delete', 2), ('insert', 2, 5), ('insert', 2, 1), ('sample', 1)])
Expected Output: [2]
Explanation: Deleting a missing item does nothing; later update sets ID 2's weight to 1.
Input: (6, [('insert', 5, 4), ('insert', 2, 2), ('sample', 3), ('delete', 5), ('insert', 6, 1), ('sample', 2)])
Expected Output: [5, 2]
Explanation: The third ticket initially falls inside ID 5's range, but after deletion the second ticket falls inside ID 2's range.
Input: (1, [('insert', 1, 7), ('sample', 7), ('delete', 1)])
Expected Output: [1]
Explanation: Edge case with only one possible item.
Solution
def solution(n, operations):
bit = [0] * (n + 1)
weights = [0] * (n + 1)
def add(i, delta):
while i <= n:
bit[i] += delta
i += i & -i
def find_by_prefix(target):
idx = 0
curr = 0
bit_mask = 1
while (bit_mask << 1) <= n:
bit_mask <<= 1
while bit_mask:
nxt = idx + bit_mask
if nxt <= n and curr + bit[nxt] < target:
idx = nxt
curr += bit[nxt]
bit_mask >>= 1
return idx + 1
ans = []
for op in operations:
if op[0] == 'insert':
_, item_id, weight = op
delta = weight - weights[item_id]
weights[item_id] = weight
add(item_id, delta)
elif op[0] == 'delete':
_, item_id = op
if weights[item_id] != 0:
add(item_id, -weights[item_id])
weights[item_id] = 0
else:
_, r = op
ans.append(find_by_prefix(r))
return ansTime complexity: O(q log n), where q is the number of operations. Space complexity: O(n).
Hints
- You need fast prefix-sum updates and prefix-sum searches. A Fenwick tree or segment tree is a natural fit.
- To answer sample(r), find the first index whose prefix sum is at least r.
Part 4: Alert on Overloaded Exchanges in a Sliding Window
Constraints
- 1 <= window_size <= 10^9
- 0 <= threshold <= 10^5
- 0 <= number of events <= 2 * 10^5
- Events are sorted by nondecreasing time
Examples
Input: (10, 2, [(1, 'A', 'X'), (2, 'B', 'X'), (3, 'C', 'X'), (20, 'D', 'X'), (21, 'E', 'X'), (22, 'F', 'X')])
Expected Output: [(3, 'X'), (22, 'X')]
Explanation: Exchange X exceeds 2 distinct apps at time 3, later recovers after old events expire, and exceeds again at time 22.
Input: (5, 1, [(1, 'app1', 'ex1'), (2, 'app1', 'ex1'), (3, 'app2', 'ex1')])
Expected Output: [(3, 'ex1')]
Explanation: The duplicate mapping from app1 does not increase the distinct-app count.
Input: (3, 1, [(1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'X'), (4, 'D', 'Y')])
Expected Output: [(3, 'X'), (4, 'Y')]
Explanation: Each exchange becomes overloaded independently inside its own window.
Input: (4, 2, [])
Expected Output: []
Explanation: Edge case: no events means no alerts.
Solution
def solution(window_size, threshold, events):
from collections import defaultdict, deque
queues = defaultdict(deque)
counts = defaultdict(dict)
distinct = defaultdict(int)
overloaded = defaultdict(bool)
alerts = []
for t, app, ex in events:
q = queues[ex]
app_counts = counts[ex]
expire_before = t - window_size + 1
while q and q[0][0] < expire_before:
old_t, old_app = q.popleft()
app_counts[old_app] -= 1
if app_counts[old_app] == 0:
del app_counts[old_app]
distinct[ex] -= 1
if distinct[ex] <= threshold:
overloaded[ex] = False
q.append((t, app))
if app not in app_counts:
app_counts[app] = 0
distinct[ex] += 1
app_counts[app] += 1
if distinct[ex] > threshold and not overloaded[ex]:
alerts.append((t, ex))
overloaded[ex] = True
return alertsTime complexity: O(m), where m is the number of events. Space complexity: O(m).
Hints
- Process each exchange independently using a queue of recent events.
- Store per-application counts inside the window so duplicates from the same application do not increase the distinct count.
Part 5: Task Queue with Insert, Delete, and Execute
Constraints
- 0 <= number of operations <= 2 * 10^5
- Priority values fit in standard integer range
- task_id values are integers
- Delete on a missing task should be ignored
Examples
Input: [('insert', 1, 5), ('insert', 2, 3), ('execute',), ('execute',)]
Expected Output: [1, 2]
Explanation: Higher priority runs first.
Input: [('insert', 1, 4), ('insert', 2, 4), ('delete', 1), ('execute',), ('execute',)]
Expected Output: [2, None]
Explanation: Task 1 is deleted before execution; the second execute sees an empty queue.
Input: [('insert', 1, 1), ('insert', 1, 10), ('insert', 2, 5), ('execute',), ('execute',)]
Expected Output: [1, 2]
Explanation: Reinserting task 1 replaces its old version with a higher-priority new version.
Input: [('execute',)]
Expected Output: [None]
Explanation: Edge case: executing from an empty queue.
Solution
def solution(operations):
import heapq
heap = []
active = {}
seq = 0
ans = []
for op in operations:
if op[0] == 'insert':
_, task_id, priority = op
seq += 1
active[task_id] = (priority, seq)
heapq.heappush(heap, (-priority, seq, task_id))
elif op[0] == 'delete':
_, task_id = op
active.pop(task_id, None)
else:
while heap:
neg_priority, insert_seq, task_id = heapq.heappop(heap)
if task_id in active and active[task_id] == (-neg_priority, insert_seq):
del active[task_id]
ans.append(task_id)
break
else:
ans.append(None)
return ansTime complexity: O(q log q), where q is the number of operations. Space complexity: O(q).
Hints
- A heap gives the next candidate quickly, but deletions and replacements leave stale entries behind.
- Keep the current active version of each task in a dictionary and discard stale heap entries when popped.