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:
alloc x: find the leftmost run ofxcontiguous free cells and occupy them, return the start index of that run, and assign this allocation an increasing allocation ID; if no such run exists, return-1.erase ID: free the entire block previously allocated under that ID, return the freed length; if the ID does not exist or was already freed, return-1.
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:
ADD(pos): insert a new obstacle atpos, keeping the array sorted.QUERY(x, size): decide whether an object of lengthsizecan be placed to end just beforex, i.e. whether the interval[x - size, x - 1]is free of any obstacle.
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
- Make the first two fast and solid: Q1 and Q2 are both single-pass basics. Practice until you write them on sight and reflexively cover edges like empty string, a contained
0, and taking the rightmost on a tie. Clear them within 20 minutes to leave room for the rest. - Simulation problems are about keeping state clean: Q3's alloc/free is covered by one occupancy array plus one ID map. Before coding, sort out the two main threads, how to find contiguous free space and how to reclaim by ID, and you dodge a lot of index bugs.
- For data-structure problems, think read/write complexity first: Q4's core is "sorted structure + binary-search query". Default to
bisect, then decide whether to reach forSortedListbased on how frequent the insertions are. - Self-test edges before submitting: CodeSignal must pass every case, so feed a few extreme inputs (empty, single element, fully occupied, out-of-range query) by hand before you hit submit.
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
- WeChat: Coding0201
- Email: [email protected]
- Telegram: @OAVOProxy