PracHub
QuestionsPremiumLearningGuidesCheatsheetNEWCoaches

Quick Overview

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.

  • medium
  • Citadel
  • Coding & Algorithms
  • Software Engineer

Implement task queue with insert, delete, execute

Company: Citadel

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Onsite

## Problem: Task manager with insert/delete/execute-next Design a data structure to manage executable tasks. Each task has: - `taskId` (unique) - `priority` (higher means executed earlier) - `createdAt` (or `seq`) to break ties among equal priority (earlier first) Support operations: 1. `add(taskId, priority)` 2. `remove(taskId)` (task may or may not exist; removing a task prevents future execution) 3. `executeNext() -> taskId | null` - Returns and removes the highest-priority remaining task. - Tie-breaker: smaller `createdAt/seq` first. ### Requirements - Aim for **O(log n)** per operation. - You may not linearly scan all tasks on `executeNext()`. ### Notes - Explain how you handle `remove()` efficiently even if the task is not at the top of the heap when removed.

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

Given an n x n 2048 board and a string of moves, simulate the board after applying all moves in order. A move is one of 'L', 'R', 'U', or 'D'. On each move, all tiles slide as far as possible in that direction, and equal-valued tiles that collide merge into one tile with double value. A tile can merge at most once per move. Do not spawn any new tiles.

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 board

Time complexity: O(n^2 * m), where m = len(moves). Space complexity: O(n^2).

Hints

  1. Solve one row or one column at a time: remove zeros, merge adjacent equal values once, then pad with zeros.
  2. 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

Implement reversible compression for a 4 x 4 2048 board. Each cell is stored in 4 bits using its exponent: 0 is stored as 0, and a tile value 2^e is stored as e. Cells are packed in row-major order, with the first cell stored in the lowest 4 bits. If mode is 'encode', convert the board into a non-negative integer. If mode is 'decode', convert the integer back into the board.

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

  1. A 2048 tile is fully determined by its exponent, so 4 bits per cell are enough here.
  2. For cell index i in row-major order, use bit positions [4*i, 4*i+3].

Part 3: Dynamic Weighted Sampling with Insert/Delete

You manage items with IDs from 1 to n. Each active item has a positive weight. Process operations of three types: ('insert', id, weight) inserts a new item or updates an existing item's weight; ('delete', id) removes the item if it exists; ('sample', r) returns the ID selected by weighted random sampling if the random ticket chosen were exactly r, where r is 1-indexed from 1 to totalWeight. Sampling is based on IDs in ascending order: return the smallest ID whose prefix sum of weights is at least r.

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 ans

Time complexity: O(q log n), where q is the number of operations. Space complexity: O(n).

Hints

  1. You need fast prefix-sum updates and prefix-sum searches. A Fenwick tree or segment tree is a natural fit.
  2. 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

You receive mapping events in chronological order. Each event is (time, application, exchange), meaning that at this time the application was mapped to that exchange. For each exchange, consider only events in the last window_size seconds inclusive, i.e. times in [t - window_size + 1, t]. If the number of distinct applications mapped to an exchange becomes greater than threshold, emit an alert (time, exchange). To avoid repeated alerts, only emit when the exchange transitions from safe to overloaded. Repeated events from the same application count once toward the distinct-application total while they remain in the 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 alerts

Time complexity: O(m), where m is the number of events. Space complexity: O(m).

Hints

  1. Process each exchange independently using a queue of recent events.
  2. 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

Implement a task queue that supports three operations: ('insert', task_id, priority), ('delete', task_id), and ('execute',). The queue always executes the active task with the largest priority. If several active tasks have the same priority, execute the one that was inserted earlier among the currently active versions. If a task_id is inserted again while already active, it replaces the old active version and gets a new insertion time. Deleting a missing task does nothing. Executing an empty queue returns None.

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 ans

Time complexity: O(q log q), where q is the number of operations. Space complexity: O(q).

Hints

  1. A heap gives the next candidate quickly, but deletions and replacements leave stale entries behind.
  2. Keep the current active version of each task in a dictionary and discard stale heap entries when popped.
Last updated: Apr 19, 2026

Loading coding console...

PracHub

Master your tech interviews with 7,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Implement a single-producer multi-consumer ring buffer - Citadel (medium)
  • Sort a Nearly Sorted Array - Citadel (hard)
  • Compute BBO and NBBO from order data - Citadel (medium)
  • Design dynamic weighted random sampling with updates - Citadel (medium)
  • Compute maximum later-earlier difference - Citadel (medium)