← Back to blog TikTok CodeSignal OA: Four Problems Debrief
TikTok

TikTok CodeSignal OA: Four Problems Debrief

2026-08-13

A while back I applied for a backend role at TikTok, and after my resume cleared I got an invite for a CodeSignal assessment. This debrief covers the four problems I hit in that OA, the platform format, and a complete solution for each one. One note worth calling out: CodeSignal recently refreshed the UI of the General Coding Assessment and rotated some of its problems, but the classics in the shared question bank still recur constantly. Three of my four this time could be matched to older discussions. If you're prepping for TikTok's OA too, or looking for OA assist / OA live support to sharpen your pace and approach together, I hope this note helps.

Environment and Format Overview

The whole assessment ran on the CodeSignal General Coding Assessment (shared question bank), timed at 70 minutes, with four problems that ramp up in difficulty: the first two are warm-up Easy problems, the third is a matrix/array simulation, and the fourth is a tougher data-structure problem. CodeSignal records your screen and monitors tab switching throughout, so try not to leave the assessment page mid-run.

Item Details
Platform CodeSignal General Coding Assessment (shared question bank)
Total time 70 minutes
Count 4 problems
Difficulty Ramping (Easy to simulation to data structure)
Focus Basic implementation speed, edge handling, simulation and query efficiency

Pacing tip: clear the first two Easy problems within about 20 minutes and save the time for problems 3 and 4. CodeSignal problems must actually pass all the test cases, so before submitting, dry-run a few edge examples in your head first.


Problem 1: Digit Product Minus Sum

Background

Given a positive integer n, compute the product of its decimal digits and the sum of its digits, and return "product minus sum". For example 123456: the product is 1*2*3*4*5*6 = 720, the sum is 1+2+3+4+5+6 = 21, so the result is 720 - 21 = 699. Now consider 1010: because it contains a 0, the product collapses to 0, the sum is 0+1+0+1 = 2, so the result is 0 - 2 = -2.

Approach

Turn the integer into a string, then convert each character back to an int: list(map(int, str(n))). A single pass accumulates both the product and the sum. Seed the product with 1; the moment any digit is 0, the product naturally becomes 0, so no special case is needed.

Python Solution

def digit_product_minus_sum(n: int) -> int:
    """Return the product of n's digits minus the sum of its digits.

    If any digit is 0, the product naturally becomes 0, no special case.
    """
    digits = list(map(int, str(n)))  # split out each digit

    product = 1
    total = 0
    for d in digits:
        product *= d  # multiply in; a 0 zeroes it out naturally
        total += d    # add up

    return product - total

Dry Run Walkthrough

Walking through n = 123456:

Step Current digit product total Note
Init 1 0 product starts at 1
Read 1 1 1 1
Read 2 2 2 3
Read 3 3 6 6
Read 4 4 24 10
Read 5 5 120 15
Read 6 6 720 21

Final result 720 - 21 = 699.

Verify the edge case containing a 0, n = 1010: when the pass reaches the second digit 0, product becomes zero, and no later multiplication can revive it. The final total = 2, so it returns 0 - 2 = -2, as expected.

Time complexity: O(log n), the digit count is proportional to the log of n. Space complexity: O(log n) to hold the digits.


Problem 2: Longest Character Run

Background

Given a lowercase-only string, find the longest run of a single repeated character; if several runs tie for longest, take the rightmost one; return the concatenation of "character + length", e.g. three consecutive c returns "c3".

Approach

Scan linearly, tracking the current run's character cur_char and length cur_len: when the character matches the previous one, cur_len += 1; otherwise reset cur_len to 1 and update cur_char. Keep the global best in best_len / best_char. The key to taking the rightmost on a tie is comparing with >=: whenever a run of equal length appears, the later one overwrites the earlier.

Python Solution

def longest_run(s: str) -> str:
    """Return the longest run of a repeated char, rightmost one on a tie.

    Using >= lets a later run of equal length override an earlier one.
    """
    if not s:
        return ""  # empty string returns empty

    cur_char = s[0]
    cur_len = 1
    best_char = s[0]
    best_len = 1

    for ch in s[1:]:
        if ch == cur_char:
            cur_len += 1  # extend the current run
        else:
            cur_char = ch  # start a new run
            cur_len = 1
        if cur_len >= best_len:  # >= lets the rightmost tie win
            best_len = cur_len
            best_char = cur_char

    return f"{best_char}{best_len}"

Dry Run Walkthrough

Walking through s = "aabbbxxccc" (bbb and ccc tie at length 3) at the key points:

Position Char cur_char / cur_len best_char / best_len Note
0 a a / 1 a / 1 init
1 a a / 2 a / 2 extend
4 b b / 3 b / 3 first run of 3
7 c c / 1 b / 3 new c run
9 c c / 3 c / 3 ties at 3, >= overwrites to c

Final result "c3": bbb and ccc are both length 3, so we take the rightmost, ccc.

Time complexity: O(n), a single pass. Space complexity: O(1), only a constant number of variables.


Problem 3: Memory Alloc/Free Simulation

Background

Simulate the allocation and freeing of a region of memory. Given a sequence of operations:

Approach

Use a boolean array to represent whether each cell is occupied. On alloc, scan left to right counting the current contiguous free length, resetting the count to zero on any occupied cell; once the count reaches x, mark that segment occupied and store id -> (start, length) in a hash map. On erase, look up the segment by ID in the hash map, clear those cells, then delete the mapping.

Python Solution

class MemoryRegion:
    """Simulate alloc / free over a contiguous region of memory.

    used[i] marks whether cell i is occupied; blocks maps id -> (start, length).
    """

    def __init__(self, size: int):
        self.used = [False] * size
        self.blocks = {}       # id -> (start, length)
        self.next_id = 0       # increasing allocation ID

    def alloc(self, x: int) -> int:
        """Occupy the leftmost x contiguous free cells, return start; -1 if none."""
        run = 0
        for i in range(len(self.used)):
            if self.used[i]:
                run = 0            # hit an occupied cell, reset the run
            else:
                run += 1
                if run == x:       # gathered x contiguous free cells
                    start = i - x + 1
                    for j in range(start, i + 1):
                        self.used[j] = True
                    self.blocks[self.next_id] = (start, x)
                    self.next_id += 1
                    return start
        return -1                  # not enough contiguous free space

    def erase(self, block_id: int) -> int:
        """Free the whole block for this ID, return its length; -1 if invalid."""
        if block_id not in self.blocks:
            return -1
        start, length = self.blocks.pop(block_id)
        for j in range(start, start + length):
            self.used[j] = False
        return length

Dry Run Walkthrough

Assume a memory size of 8, running the operations in order:

Operation Memory state (1=occupied) Returns Note
Init 00000000 all free
alloc 3 11100000 0 leftmost 3 free, allocation ID 0
alloc 2 11111000 3 continues to the right, ID 1
erase 0 00011000 3 frees the 3 cells of ID 0
alloc 4 00011111 4 left side only has 3 free, lands at index 4
erase 5 00011111 -1 ID 5 does not exist

Time complexity: alloc is O(n) (worst case scans the whole region), erase is O(len) (clears the matching segment). Space complexity: O(n) for the occupancy array and the allocation map.


Problem 4: Obstacle Range Queries

Background

In the shared question bank, this problem's statement was mislabeled: the source text paired Q4 with Q1's description, so the real Q4 has to be reconstructed from its solution. It actually tests an obstacle-placement / range-query problem: you maintain a sorted array of obstacle positions and support two operations:

Approach

Use bisect.insort to keep the array sorted; insertion is O(n) due to array shifting. On a query, binary-search for the rightmost obstacle at a position at or below x - 1, then check whether it falls inside [x - size, x - 1]: if it does, the object cannot fit, so return False; otherwise return True. The query is O(log n).

Tip: if the interview further requires O(log n) insertion, swap the sorted array for a balanced binary search tree / ordered set (e.g. sortedcontainers.SortedList), so both insertion and query reach O(log n).

Python Solution

import bisect


class ObstacleField:
    """Maintain sorted obstacle positions with add and free-range queries."""

    def __init__(self):
        self.obstacles = []  # always kept in ascending order

    def add(self, pos: int) -> None:
        """Insert an obstacle, keeping the array sorted (O(n) array shift)."""
        bisect.insort(self.obstacles, pos)

    def query(self, x: int, size: int) -> bool:
        """Decide whether [x - size, x - 1] is free of any obstacle."""
        left = x - size
        right = x - 1
        # find the first index > right; the one before it is the rightmost <= right
        idx = bisect.bisect_right(self.obstacles, right) - 1
        if idx < 0:
            return True  # no obstacle at or left of right
        nearest = self.obstacles[idx]
        return nearest < left  # inside [left, right] means it cannot fit

Dry Run Walkthrough

Running ADD 2, ADD 5, ADD 9 in order, the array becomes [2, 5, 9], then a few queries:

Operation Target interval [x-size, x-1] Nearest obstacle Returns Note
QUERY(x=5, size=2) [3, 4] 2 (rightmost <=4) True 2 < 3, interval is free
QUERY(x=6, size=3) [3, 5] 5 False 5 lands inside [3,5]
QUERY(x=2, size=1) [1, 1] none (idx<0) True nothing left of 1
QUERY(x=10, size=1) [9, 9] 9 False 9 lands inside [9,9]

Time complexity: query is O(log n) (binary search); add is O(n) (array shift). With an ordered set, add also drops to O(log n). Space complexity: O(n) to hold the obstacle positions.


Prep Strategy


FAQ

Q1: Do CodeSignal problems have to pass all the test cases?

Yes. Unlike some live rounds that only weigh your thinking, the CodeSignal General Coding Assessment scores by the hidden cases you pass, so before submitting always dry-run a few edge examples and confirm the logic holds.

Q2: Do the old problems from the shared bank still show up?

They do. Even though the UI was refreshed and some problems were rotated, the classics in the shared question bank still recur often. Three of my four this time matched older discussions, so a pass over the high-frequency problems pays off.

Q3: In Q3's memory simulation, how do you handle alloc failing to find enough space?

If, after scanning the whole region, the contiguous free length never reaches x, return -1 and leave all state untouched. Note that you must reset the run count to zero the moment you hit an occupied cell.

Q4: Q4's statement was mislabeled, so how do you tell what it really tests?

The shared bank occasionally shows a statement that does not match its test cases. When that happens, treat the test cases and the function signature as the source of truth and reverse-engineer the real logic. This problem's ADD / QUERY semantics were reconstructed from the cases into the obstacle range query.

Q5: For Q4, is a sorted array or an ordered set better?

If queries far outnumber insertions, bisect plus a sorted array is enough, with O(log n) query and O(n) insertion. If insertions are also frequent, switch to sortedcontainers.SortedList so both insertion and query reach O(log n).


Prepping for TikTok's OA? We know the CodeSignal General Coding Assessment's question bank and pace, and can provide end-to-end OA assist / OA live support to sharpen your high-frequency problem types and edge handling in one pass.

Add WeChat Coding0201 now to get a one-on-one custom prep plan.

Contact