Conceptual Foundation & Intuitive Analogy
🧭 Conceptual Foundation & Pattern Intuition
Traversing an 2D matrix in spiral order is not about calculating complex geometric curves or calculating trigonometric trajectories. It is an exercise in Rectangular Boundary Contraction. Four boundary walls—top, bottom, left, right—define the unvisited rectangular domain. Traversal proceeds along the active perimeter, and as each side is completed, its boundary wall is shifted inward monotonically.
🏟️ The Analogy: Peeling an Onion, Layer by Layer
Imagine peeling an onion with flat, rectangular layers. You slice off the top outer skin (traverse left to right), slice off the right skin (traverse top to bottom), shave off the bottom skin (traverse right to left), and finally peel up the left skin (traverse bottom to top). What remains inside is another smaller, pristine rectangular onion. You repeat this peeling cycle until nothing remains.
🔑 Why It Works
Notice the crucial safety checks if top <= bottom and if left <= right before the backward passes! For non-square matrices (e.g., or ), the first two sweeps contract the boundary past the opposing wall. Without these guards, the backward loops would re-traverse already visited cells, creating duplicate elements.
Boundary pointers (top, bottom, left, right) contract inward monotonically after each directional sweep.
Boundary pointers contract inward after each directional sweep; modular arithmetic bounds state cyclically.
while top <= bottom and left <= right: sweep right, down, left, up, contracting respective pointer.
The 11 Mental Models of Math and Geometry
Categorize math problems by their spatial or arithmetic invariant before writing code.
1Digit Manipulation
integer x ➔ digit modulo extraction ➔ overflow clampInteger Overflow Handling & Digit Manipulation
| 1 | def reverse_integer(x: int) -> int: |
| 2 | """Reverses digits of a 32-bit signed integer with overflow detection. |
| 3 | Time: O(log10 |x|) | Space: O(1) |
| 4 | """ |
| 5 | INT_MAX = 2**31 - 1 # 2147483647 |
| 6 | INT_MIN = -2**31 # -2147483648 |
| 7 | |
| 8 | # 1. Normalize the Sign: |
| 9 | # Python's % and // round toward minus infinity, so working on a |
| 10 | # negative x directly would misbehave; the sign is split off and reapplied at the end. |
| 11 | sign = -1 if x < 0 else 1 |
| 12 | x = abs(x) |
| 13 | res = 0 # digits of x, read from the right, rebuilt in reverse order |
| 14 | |
| 15 | # 2. Peel Off and Append One Digit at a Time: |
| 16 | # x % 10 reads the last digit and x //= 10 drops it; appending that |
| 17 | # digit to res with res * 10 + digit builds the reversed number one digit per step. |
| 18 | while x > 0: |
| 19 | digit = x % 10 |
| 20 | x //= 10 |
| 21 | |
| 22 | # 3. Detect Overflow Before It Happens: |
| 23 | # res * 10 + digit must stay within INT_MAX; checking res against |
| 24 | # INT_MAX // 10 first catches the overflow before the multiply can occur. |
| 25 | if res > INT_MAX // 10 or (res == INT_MAX // 10 and digit > 7): |
| 26 | return 0 |
| 27 | |
| 28 | res = res * 10 + digit |
| 29 | |
| 30 | # 4. Reapply the Sign and the Final Range Check: |
| 31 | # The sign set aside in step 1 is multiplied back in, then the result |
| 32 | # is re-checked against the full signed range in case that flipped it out of bounds. |
| 33 | res *= sign |
| 34 | return res if INT_MIN <= res <= INT_MAX else 0 |
Treat a number as a row of digits: x % 10 reads the last digit, x //= 10 drops it, and res = res * 10 + digit appends a digit on the right, so any number can be taken apart and rebuilt one digit at a time without turning it into a string. Each step moves exactly one digit, so reading from the right while appending builds the digits in reverse order, as reverse_integer does; the same two moves, in other orders, compare a number with its mirror, add numbers stored as digit arrays or parse text into a number. Work on abs(x) and put the sign back at the end, because in Python % and // round toward minus infinity for negatives (-7 % 10 == 3). The trap is overflow: test res > INT_MAX // 10 (or res == INT_MAX // 10 and digit > 7) before res * 10 + digit, because with fixed-width integers the multiply itself overflows: Java silently wraps the value around, and in C++ signed overflow is undefined behavior.
Gotcha 1 of 3
- The trap
Gotcha 1: Checking the 32-bit limit after
res * 10 + digitMistakeres = res * 10 + digit if res > INT_MAX: return 0Fixif res > INT_MAX // 10 or (res == INT_MAX // 10 and digit > 7): return 0 res = res * 10 + digitWhat happens: In Java or C++,
resis a 32-bit integer.res * 10 + digitcan go pastINT_MAX, which is 2^31 - 1. Java then wraps the value around, and in C++ signed overflow is undefined behavior. A check after the multiply only sees the wrapped value. For x = 1534236469, the true reverse is 9646324351, and Java's wrapped value is 1056389759. That value is belowINT_MAX, so the late check lets it through. Python ints never overflow, so in Python both orders return 0. The check before the multiply is the one that works in every language.
2Spiral Matrix
grid (R x C) ➔ top/bottom/left/right ➔ inward spiralSpiral Matrix Traversal (Layer-by-Layer Boundary Shrinking)
| 1 | def spiral_order(matrix: list[list[int]]) -> list[int]: |
| 2 | """Spiral Matrix: read the matrix ring by ring with four boundaries. |
| 3 | Time: O(R * C) | Space: O(1) auxiliary |
| 4 | """ |
| 5 | if not matrix or not matrix[0]: |
| 6 | return [] |
| 7 | |
| 8 | top, bottom = 0, len(matrix) - 1 |
| 9 | left, right = 0, len(matrix[0]) - 1 |
| 10 | result: list[int] = [] |
| 11 | |
| 12 | while top <= bottom and left <= right: |
| 13 | # 1. Sweep Top (Left -> Right) |
| 14 | for c in range(left, right + 1): |
| 15 | result.append(matrix[top][c]) |
| 16 | top += 1 |
| 17 | |
| 18 | # 2. Sweep Right (Top -> Bottom) |
| 19 | for r in range(top, bottom + 1): |
| 20 | result.append(matrix[r][right]) |
| 21 | right -= 1 |
| 22 | |
| 23 | # 3. Sweep Bottom (Right -> Left) with Guard! |
| 24 | if top <= bottom: |
| 25 | for c in range(right, left - 1, -1): |
| 26 | result.append(matrix[bottom][c]) |
| 27 | bottom -= 1 |
| 28 | |
| 29 | # 4. Sweep Left (Bottom -> Top) with Guard! |
| 30 | if left <= right: |
| 31 | for r in range(bottom, top - 1, -1): |
| 32 | result.append(matrix[r][left]) |
| 33 | left += 1 |
| 34 | |
| 35 | return result |
Walk the matrix one ring at a time with four boundaries, top, bottom, left and right, that close in from all four sides like converging pointers, and move each boundary inward right after its side is read. Each boundary marks the first row or column not yet read on its side, so after the four sweeps the unread cells form a smaller rectangle and the same sweeps repeat on it. The trap is the last, thin ring: after the top and right sweeps there may be no row or no column left, so guard the bottom sweep with if top <= bottom and the left sweep with if left <= right, or a single row such as [[1,2,3]] returns [1,2,3,2,1].
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out the
if top <= bottomguardMistakefor c in range(right, left - 1, -1): result.append(matrix[bottom][c]) bottom -= 1Fixif top <= bottom: for c in range(right, left - 1, -1): result.append(matrix[bottom][c]) bottom -= 1What happens: After the top sweep and the right sweep, the rows may be used up. For
[[1,2,3]], the top sweep reads row 0 and setstopto 1, whilebottomis 0. Without the guard, the bottom sweep reads row 0 again, backwards. The left sweep needsif left <= rightfor the same reason.Example: matrix = [[1, 2, 3]]Mistake: returns [1, 2, 3, 2, 1]Fix: returns [1, 2, 3]
3Fast Power
base x, exponent n ➔ halve exponent repeatedly ➔ O(log N) powerFast Power (Exponentiation by Squaring)
| 1 | def my_pow(x: float, n: int) -> float: |
| 2 | """Binary Exponentiation: computes x^n by squaring. |
| 3 | Time: O(log N) | Space: O(1) |
| 4 | """ |
| 5 | if n < 0: |
| 6 | x = 1.0 / x # x^-n == (1/x)^n |
| 7 | n = -n |
| 8 | |
| 9 | result = 1.0 |
| 10 | curr_product = x # Holds x^(2^i) for the current bit i |
| 11 | |
| 12 | while n > 0: |
| 13 | if n % 2 == 1: |
| 14 | result *= curr_product # Bit is set: multiply this power in |
| 15 | curr_product *= curr_product # Square for the next bit |
| 16 | n //= 2 # Move to the next bit |
| 17 | |
| 18 | return result |
Write n in binary: x^13 = x^8 · x^4 · x^1, so only the powers x^1, x^2, x^4, ... are ever needed. curr_product squares once per bit, and result takes it in only when the current bit of n is 1 (n % 2 == 1). It is correct because result * curr_product ** n stays equal to the original x ** n every round, and the loop ends with n = 0. The trap is a negative n: flip first with x = 1 / x and n = -n (in Java or C++ copy n into 64 bits first, since -n overflows for -231).
Gotcha 1 of 4
- The trap
Gotcha 1: Negating
nwithout replacingxby1 / xMistakeif n < 0: n = -nFixif n < 0: x = 1.0 / x n = -nWhat happens: For a negative
n,x ** nequals(1 / x) ** -n. The loop only handlesn >= 0. Withn = -nalone, it computesx ** -n. Somy_pow(2.0, -2)returns 4.0 instead of 0.25.Example: x = 2.0, n = -2Mistake: returns 4.0Fix: returns 0.25
4In-Place Marking
matrix ➔ markers in the first row / column or spare bits ➔ apply in a second passIn-Place Markers (First Row/Column Flags, Two States per Cell)
O(1) extra memory, but each change depends on the input as it was: rows or columns to wipe, or a new state that every neighbour must still read in its old form.| 1 | def set_zeroes(matrix: list[list[int]]) -> None: |
| 2 | """Set Matrix Zeroes: row 0 and column 0 hold the flags for the other rows and columns. |
| 3 | Time: O(M * N) | Space: O(1) |
| 4 | """ |
| 5 | m, n = len(matrix), len(matrix[0]) |
| 6 | |
| 7 | # 1. Save Row 0's and Column 0's Own Zeros First: |
| 8 | # Pass 1 below overwrites row 0 and column 0 with flags, so whether |
| 9 | # they originally held a zero must be recorded before any flag is written. |
| 10 | first_row_zero = any(matrix[0][c] == 0 for c in range(n)) |
| 11 | first_col_zero = any(matrix[r][0] == 0 for r in range(m)) |
| 12 | |
| 13 | # 2. Mark Flags in Row 0 and Column 0: |
| 14 | # A zero found inside the matrix (outside row 0 and column 0) means its |
| 15 | # whole row and column end up zero, so that is recorded by zeroing the |
| 16 | # row's cell in column 0 and the column's cell in row 0. |
| 17 | for r in range(1, m): |
| 18 | for c in range(1, n): |
| 19 | if matrix[r][c] == 0: |
| 20 | matrix[r][0] = 0 |
| 21 | matrix[0][c] = 0 |
| 22 | |
| 23 | # 3. Apply the Flags to Every Inner Cell: |
| 24 | # Any inner cell whose row flag or column flag is set is zeroed; row 0 |
| 25 | # and column 0 themselves are skipped here because they still need to be read. |
| 26 | for r in range(1, m): |
| 27 | for c in range(1, n): |
| 28 | if matrix[r][0] == 0 or matrix[0][c] == 0: |
| 29 | matrix[r][c] = 0 |
| 30 | |
| 31 | # 4. Clear Row 0 and Column 0 Last: |
| 32 | # Row 0 and column 0 are cleared only after every other cell has read |
| 33 | # their flags; clearing them earlier would turn every flag true and wipe the whole matrix. |
| 34 | if first_row_zero: |
| 35 | for c in range(n): |
| 36 | matrix[0][c] = 0 |
| 37 | if first_col_zero: |
| 38 | for r in range(m): |
| 39 | matrix[r][0] = 0 |
When no extra memory is allowed, store the marks in cells whose final value you already know: a zero at (r, c) means row r and column c end up all zero, so pass 1 writes the flags matrix[r][0] = 0 and matrix[0][c] = 0, and pass 2 zeroes every cell whose row flag or column flag is 0. Writing a flag loses nothing, because both flag cells become zero in the answer anyway; the same idea packs two states into one cell when every neighbour must still read it in its old form, because the old value stays readable until every reader is done, whether that means a whole cell repurposed as a flag or one bit of it holding the next state. The trap is that row 0 and column 0 are data as well as flags: compute first_row_zero and first_col_zero before pass 1 writes any flag, keep both passes to r >= 1 and c >= 1, and clear row 0 and column 0 last, because clearing them first turns every flag to 0 and wipes the whole matrix.
Gotcha 1 of 5
- The trap
Gotcha 1: Clearing row 0 before pass 2 reads the flags
Mistakeif first_row_zero: for c in range(n): matrix[0][c] = 0 for r in range(1, m): for c in range(1, n): if matrix[r][0] == 0 or matrix[0][c] == 0: matrix[r][c] = 0Fixfor r in range(1, m): for c in range(1, n): if matrix[r][0] == 0 or matrix[0][c] == 0: matrix[r][c] = 0 if first_row_zero: for c in range(n): matrix[0][c] = 0What happens: After pass 1, row 0 holds the column flags. A 0 at
matrix[0][c]means "clear columnc". If row 0 is cleared before pass 2, everymatrix[0][c]becomes 0. Pass 2 then finds a flag in every column and clears the whole matrix.Example: matrix = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]Mistake: returns [[0, 0, 0], [0, 0, 0], [0, 0, 0]]Fix: returns [[0, 0, 0], [0, 4, 5], [0, 7, 8]]
5Cross Product
three points ➔ sign of the cross product ➔ left turn, right turn or straight lineCross Product Orientation Test (Turn Direction, Collinearity, Convexity)
| 1 | def cross(o: list[int], a: list[int], b: list[int]) -> int: |
| 2 | """Orientation test: > 0 when o -> a -> b turns left, < 0 when it turns right, 0 when the points are collinear.""" |
| 3 | # cross is twice the signed area of triangle o, a, b: its sign alone gives the turn direction. |
| 4 | return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) |
| 5 | |
| 6 | |
| 7 | def is_convex(points: list[list[int]]) -> bool: |
| 8 | """Convex Polygon: every turn along the boundary has the same cross-product sign. |
| 9 | Time: O(N) | Space: O(1) |
| 10 | """ |
| 11 | n = len(points) |
| 12 | prev = 0 # prev: the last non-zero turn seen so far; 0 means no turn has been seen yet |
| 13 | for i in range(n): |
| 14 | # 1. Compute the Turn at the Next Corner: |
| 15 | # cross() gives the sign of the turn o -> a -> b in one integer; % n wraps the index, so the |
| 16 | # two turns that close the polygon are checked in the same pass as every other corner. |
| 17 | turn = cross(points[i], points[(i + 1) % n], points[(i + 2) % n]) |
| 18 | |
| 19 | # 2. Classify the Turn (the trap): |
| 20 | # A turn of 0 is a straight corner: it is allowed and tells nothing about direction, so it |
| 21 | # is skipped before prev is touched. A later non-zero turn with the opposite sign means the |
| 22 | # boundary bent back on itself, a dent, so the polygon is not convex. |
| 23 | if turn == 0: |
| 24 | continue |
| 25 | if prev != 0 and (turn > 0) != (prev > 0): |
| 26 | return False |
| 27 | |
| 28 | # 3. Remember the Direction: |
| 29 | # Save this non-zero turn so the next corner's turn can be compared against it. |
| 30 | prev = turn |
| 31 | return True |
The cross product cross(o, a, b) = (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) answers "which way does the path o -> a -> b turn?" with one integer: positive for a left (counter-clockwise) turn, negative for a right turn, 0 when the three points lie on one line. A polygon is convex exactly when its boundary always turns the same way, so is_convex computes turn at every corner, wrapping around with % n so the two turns that close the loop are checked too, and returns False the first time a turn's sign differs from prev. This is correct because the value is twice the signed area of the triangle o, a, b: its sign flips exactly when b crosses from one side of the line through o and a to the other, and it uses only integer multiplication and subtraction, so there is no division by zero on a vertical edge and no rounding error. The trap is a turn of 0: three points in a row on one line make a straight corner, which is allowed and says nothing about the direction, so continue past it. Treating it as a change of direction rejects convex polygons that have extra points along an edge, and storing it in prev overwrites the record of which way the boundary was turning, so a dent right after a straight corner slips through.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
if turn == 0: continueMistakeif prev != 0 and (turn > 0) != (prev > 0):Fixif turn == 0: continue if prev != 0 and (turn > 0) != (prev > 0):What happens: A turn of 0 is a straight corner. It is allowed, and it says nothing about the direction. Without
continue, the 0 reaches the sign test. After a right turn, the test passes, andprev = turnsetsprevto 0. The last known direction is lost. A dent right after the straight corner is then not caught. After a left turn, the same 0 fails the test, so a convex polygon is rejected.Example: points = [[6,6],[8,12],[12,12],[12,0],[0,0],[0,12],[4,12],[5,9]]Mistake: returns TrueFix: returns False
6Euclid's GCD
two numbers ➔ replace (a, b) by (b, a % b) ➔ greatest common divisorEuclidean Algorithm (Greatest Common Divisor)
| 1 | def gcd(a: int, b: int) -> int: |
| 2 | """Euclid's algorithm: gcd(a, b) == gcd(b, a % b), and gcd(a, 0) == a.""" |
| 3 | # 1. Replace the Pair Until It Shrinks to Zero (the trap): |
| 4 | # a % b is a minus a multiple of b, so a number dividing both a and b also divides a % b, and a |
| 5 | # number dividing b and a % b also divides a: both values must update from the old pair at once. |
| 6 | while b: |
| 7 | a, b = b, a % b |
| 8 | # 2. Return the Answer: |
| 9 | # The loop stops once b is 0; since gcd(a, 0) == a, a now holds the greatest common divisor. |
| 10 | return a |
| 11 | |
| 12 | |
| 13 | def find_gcd(nums: list[int]) -> int: |
| 14 | """Find Greatest Common Divisor of Array: Euclid's algorithm on the smallest and largest value. |
| 15 | Time: O(N + log M) | Space: O(1) |
| 16 | """ |
| 17 | lo, hi = min(nums), max(nums) # lo, hi: the two ends; only they matter, the other values play no part |
| 18 | return gcd(hi, lo) |
Euclid's algorithm finds the greatest common divisor of two numbers by replacing the pair (a, b) with (b, a % b) until b is 0; then a is the answer. Here find_gcd only needs the smallest and largest value, lo and hi, so it returns gcd(hi, lo). It is correct because a % b is a minus a multiple of b: any number that divides both a and b also divides a % b, and any number that divides b and a % b also divides a, so the pair changes but its common divisors never do. The trap is the update: write a, b = b, a % b as one statement. Updating a = b first makes the next line compute b % b, which is always 0, so the loop stops after one step and returns the smaller number.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
a = band thenb = a % bMistakea = b b = a % bFixa, b = b, a % bWhat happens: After
a = b, the oldais gone. The next line computesb % b, which is always 0. The loop stops after one step and returns the oldb. Sogcd(8, 3)returns 3 instead of 1.Example: a = 8, b = 3Mistake: returns 3Fix: returns 1
7Canonical Key
integers (a, b) ➔ Euclidean reduction ➔ normalized coprime factorsCanonical-Form Hashing (Normalize Before You Count)
| 1 | import math |
| 2 | from collections import defaultdict |
| 3 | |
| 4 | def max_points_on_line(points: list[list[int]]) -> int: |
| 5 | """Maximum points collinear on a plane using GCD slope hashing. |
| 6 | Time: O(N^2) | Space: O(N) |
| 7 | """ |
| 8 | # 1. Handle Trivial Inputs: |
| 9 | # Any 0, 1, or 2 points are always collinear, so the general counting |
| 10 | # below is skipped for those sizes. |
| 11 | if len(points) <= 2: |
| 12 | return len(points) |
| 13 | |
| 14 | max_pts = 0 # largest count of collinear points found across all anchors |
| 15 | |
| 16 | # 2. Anchor on Each Point in Turn: |
| 17 | # slopes counts, for the current anchor points[i], how many later points |
| 18 | # share each direction from it; a fresh dict per anchor keeps directions |
| 19 | # from different anchors from being mixed into the same count. |
| 20 | for i in range(len(points)): |
| 21 | slopes: dict[tuple[int, int], int] = defaultdict(int) |
| 22 | x1, y1 = points[i] |
| 23 | |
| 24 | for j in range(i + 1, len(points)): |
| 25 | x2, y2 = points[j] |
| 26 | dx, dy = x2 - x1, y2 - y1 |
| 27 | g = math.gcd(dx, dy) |
| 28 | |
| 29 | # 3. Reduce the Direction to a Canonical Key: |
| 30 | # Dividing dx and dy by their gcd keeps the same direction in |
| 31 | # lowest terms, then flipping the sign so dy is positive (or dy |
| 32 | # == 0 with dx positive) makes opposite vectors of one line hash alike. |
| 33 | slope = (dy // g, dx // g) |
| 34 | if slope[0] < 0 or (slope[0] == 0 and slope[1] < 0): |
| 35 | slope = (-slope[0], -slope[1]) |
| 36 | |
| 37 | # 4. Count and Track the Best Line Through This Anchor: |
| 38 | # Every point sharing a key lies on the same line through |
| 39 | # points[i]; the + 1 below counts the anchor point itself. |
| 40 | slopes[slope] += 1 |
| 41 | max_pts = max(max_pts, slopes[slope] + 1) |
| 42 | |
| 43 | return max_pts |
Before counting, turn each item into a key that is equal exactly when the items should count as equal; here the item is the direction from points[i] to points[j], and its key is (dy // g, dx // g) with g = gcd(dx, dy), sign fixed. Two directions lie on the same line through points[i] exactly when their reduced integer pairs match, and integers compare exactly, so two different directions never share one bucket, as rounded float slopes can when the coordinates are large. The trap is the sign: (1, -2) and (-1, 2) are the same direction, so force dy positive (or dy == 0 with dx positive) before counting slope, or one line is counted as two.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving the sign of
(dy // g, dx // g)unfixedMistakeslope = (dy // g, dx // g)Fixslope = (dy // g, dx // g) if slope[0] < 0 or (slope[0] == 0 and slope[1] < 0): slope = (-slope[0], -slope[1])What happens:
math.gcdnever returns a negative number. Sody // ganddx // gkeep the signs ofdyanddx. Forpoints = [[0, 0], [1, 1], [-1, -1]], the anchor[0, 0]gives[1, 1]the key(1, 1). It gives[-1, -1]the key(-1, -1), although both points lie on the same line. That one line is then counted in two buckets. The fix flips the pair so thatdyis positive, ordy == 0anddxis positive.Example: points = [[0, 0], [1, 1], [-1, -1]]Mistake: returns 2Fix: returns 3
8Count and Skip
rank k in a huge list in dictionary order ➔ at each slot, skip k // block blocks of equal size (k counted from 0) and keep k % block ➔ the k-th item, built one slot at a timeK-th in Dictionary Order by Block Counting (Unranking, Factorial Number System)
k-th item of a huge list sorted in dictionary order (the k-th permutation, route, string or number), or the rank of a given item in that list, and the list is far too long to build: k can reach n!, C(30, 15) or 1015.| 1 | def get_permutation(n: int, k: int) -> str: |
| 2 | """Permutation Sequence: skip whole blocks of arrangements that share the digits chosen so far. |
| 3 | Time: O(N^2) | Space: O(N) |
| 4 | """ |
| 5 | fact = [1] * n # fact[m] = m! |
| 6 | |
| 7 | # 1. Precompute Factorials: |
| 8 | # fact[m] holds m!, the number of arrangements of m items, needed below |
| 9 | # to size each slot's blocks without computing a factorial inside the main loop. |
| 10 | for m in range(1, n): |
| 11 | fact[m] = fact[m - 1] * m |
| 12 | |
| 13 | pool = [str(d) for d in range(1, n + 1)] # unused digits, smallest first |
| 14 | |
| 15 | # 2. Convert the Rank to 0-Based: |
| 16 | # k counts from 1, but k // block below needs the count of arrangements |
| 17 | # that come strictly before the answer, which is k - 1. |
| 18 | k -= 1 |
| 19 | result = [] # digits of the answer, chosen one slot at a time |
| 20 | |
| 21 | # 3. Choose Each Slot by Skipping Whole Blocks: |
| 22 | # Every unused digit starts a block of fact[n - 1 - slot] arrangements |
| 23 | # that share the digits chosen so far; dividing k by that block size |
| 24 | # finds how many whole blocks come before the answer, and the remainder |
| 25 | # becomes the answer's rank inside the chosen block for the next slot. |
| 26 | for slot in range(n): |
| 27 | block = fact[n - 1 - slot] |
| 28 | index = k // block |
| 29 | k %= block |
| 30 | result.append(pool.pop(index)) |
| 31 | return "".join(result) |
Items in dictionary order come in blocks: every arrangement that starts with the same digit sits together, and here every block at a slot holds the same number of arrangements, block = fact[n - 1 - slot]. So the answer's digit for a slot is found without listing anything: skip index = k // block whole blocks, take pool.pop(index), and continue inside the chosen block with k %= block, one slot to the right and one factorial smaller. This is correct because the blocks come in the same order as their first digits and all have the same size, so exactly index of them lie before the answer, and k % block is the answer's rank inside its own block; removing the used digit from pool keeps the next slot's choices in order. The trap is the rank's base: k counts from 1, so subtract 1 first. Without k -= 1, every k that is a multiple of block picks one block too far: n = 3, k = 3 gives "231" instead of "213".
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
k -= 1Mistakeresult = []Fixk -= 1 result = []What happens:
kcounts from 1, butk // blockneeds the number of arrangements before the answer. That number isk - 1. Withoutk -= 1, everykthat is a multiple ofblockpicks one block too far.Example: n = 3, k = 3Mistake: returns '231'Fix: returns '213'
9Josephus Problem
circle of n friends ➔ smaller circle's winner ➔ shift by kJosephus Problem (Circular Elimination Recurrence)
| 1 | def find_the_winner(n: int, k: int) -> int: |
| 2 | """Find the Winner of the Circular Game (Josephus): f(size) = (f(size - 1) + k) % size. |
| 3 | Time: O(N) | Space: O(1) |
| 4 | """ |
| 5 | winner = 0 # 0-indexed winner of the game with 1 friend |
| 6 | for size in range(2, n + 1): |
| 7 | winner = (winner + k) % size # Shift the smaller game's winner k seats |
| 8 | return winner + 1 # Friends are numbered from 1 |
Once the first friend leaves, the rest is the same game with one friend fewer, only started k seats later, so if winner is the answer for size - 1 friends, (winner + k) % size is the answer for size friends, and a loop from size = 2 up to n builds it without simulating anyone leaving. It is correct because removing the k-th friend and restarting from the next seat is exactly a shift of the smaller circle by k seats, and % size wraps that shift around the larger circle. The trap is the index base: the rule counts seats from 0, so start from winner = 0 for one friend and add 1 only in the final return, because the friends are numbered from 1.
Gotcha 1 of 4
- The trap
Gotcha 1: Returning
winnerwithout adding 1Mistakereturn winnerFixreturn winner + 1What happens: The recurrence counts seats from 0. So
winner = 0is the only seat when one friend is left. Find the Winner of the Circular Game (LC 1823) numbers the friends from 1. Only the finalreturnadds 1.Example: n = 5, k = 2Mistake: returns 2Fix: returns 3
10Sieve of Eratosthenes
upper bound n ➔ boolean sieve array ➔ prime countSieve of Eratosthenes
| 1 | def count_primes(n: int) -> int: |
| 2 | """Sieve of Eratosthenes: counts primes strictly below n. |
| 3 | Time: O(N log log N) | Space: O(N) |
| 4 | """ |
| 5 | if n <= 2: |
| 6 | return 0 # No primes below 2 |
| 7 | |
| 8 | is_prime = [True] * n |
| 9 | is_prime[0] = is_prime[1] = False # 0 and 1 are not prime |
| 10 | |
| 11 | for p in range(2, int(n ** 0.5) + 1): # Factors above sqrt(n) were already crossed out |
| 12 | if is_prime[p]: |
| 13 | for multiple in range(p * p, n, p): # Start at p*p: smaller multiples have a smaller factor |
| 14 | is_prime[multiple] = False |
| 15 | |
| 16 | return sum(is_prime) |
A number is composite exactly when it has a prime factor no larger than its square root, so cross off multiples only for primes p up to sqrt(n). Start each crossing at p * p: every smaller multiple p * k with k < p was already crossed off by a smaller prime factor of k. Whatever is still True in is_prime is prime. The trap is 0 and 1: they start True like every slot, but no crossing ever reaches them, so set both to False first or the count comes out 2 too high.
Gotcha 1 of 5
- The trap
Gotcha 1: Leaving
is_prime[0]andis_prime[1]as TrueMistakeis_prime = [True] * nFixis_prime = [True] * n is_prime[0] = is_prime[1] = FalseWhat happens: Every slot starts True. Crossing out starts at
p * p, which is at least 4. So no step ever reaches 0 or 1. Without the line,sum(is_prime)counts 0 and 1 as primes, and the count is 2 too high.Example: n = 10Mistake: returns 6Fix: returns 4
11Reservoir Sampling
stream of items ➔ keep each with chance 1/i ➔ one uniform random itemReservoir Sampling & Randomized Pick
| 1 | import random |
| 2 | from typing import Optional |
| 3 | |
| 4 | |
| 5 | class ListNode: |
| 6 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 7 | self.val = val |
| 8 | self.next = next |
| 9 | |
| 10 | |
| 11 | class Solution: |
| 12 | """Linked List Random Node: reservoir sampling, one pass per pick and O(1) extra space. |
| 13 | Time: O(N) per getRandom | Space: O(1) |
| 14 | """ |
| 15 | |
| 16 | def __init__(self, head: Optional[ListNode]): |
| 17 | self.head = head # Keep only the head: every pick walks the list again |
| 18 | |
| 19 | def getRandom(self) -> int: |
| 20 | result, i = 0, 0 # result: the current pick; i: nodes seen so far |
| 21 | node = self.head |
| 22 | while node: |
| 23 | # 1. Count This Node: |
| 24 | # i must count node before the draw, so the first node is always taken (randrange(1) is 0). |
| 25 | i += 1 |
| 26 | |
| 27 | # 2. Draw With Chance 1/i: |
| 28 | # randrange(i) is one of 0..i - 1, so it lands on 0 with chance exactly 1/i; every earlier |
| 29 | # node then keeps its own share, since it survives this draw with chance (i - 1)/i. |
| 30 | if random.randrange(i) == 0: |
| 31 | result = node.val |
| 32 | |
| 33 | # 3. Move to the Next Node: |
| 34 | # The same draw repeats at every node until the whole list has been walked once. |
| 35 | node = node.next |
| 36 | # 4. Return the Last Survivor: |
| 37 | # After the last node every node has had chance exactly 1/n, with no copy of the list ever made. |
| 38 | return result |
Reservoir sampling picks a uniform random node in one pass without knowing the length: walk the list, count the nodes with i, and let the i-th node replace result with chance exactly 1/i, drawn as random.randrange(i) == 0. It is fair because the shares stay equal at every step: the new node gets in with 1/i, and each earlier node, which was result with chance 1/(i - 1), survives the draw with (i - 1)/i, which leaves it at 1/i too; after the last node every node has chance 1/n, with no copy of the list. The trap is the chance: i must count the current node before the draw, so the first node is always taken (randrange(1) is 0). randrange(i + 1), a chance of 1/(i + 1), keeps the head only half the time and can leave result at its starting 0, and returning at the first successful draw always returns the head.
Gotcha 1 of 3
- The trap
Gotcha 1: Drawing
randrange(i + 1)instead ofrandrange(i)Mistakeif random.randrange(i + 1) == 0:Fixif random.randrange(i) == 0:What happens:
i += 1already counts the current node, so nodeimust win with chance 1/i. The head then wins with chance 1/1, so it is always kept first. Withrandrange(i + 1), the head wins only half the time. For the list [1, 2], each of 0, 1 and 2 is returned in one third of the calls. The 0 isresult's starting value, which is not in the list.Example: head = [1, 2], the set of values from 100 callsMistake: returns [0, 1, 2]Fix: returns [1, 2]
The Math & Geometry Decision Rule
When facing an unseen mathematical or geometric problem, ask in this order:
Validate bounds before res = res * 10 + digit.
Build the key first, e.g. (dy // g, dx // g) with g = gcd(dx, dy) and one fixed sign, then count keys in a dict.
Record the changes in the first row and column (or in spare bits of each cell), then apply them in a second pass.
Establish top, bottom, left, right bounds and contract clockwise.
f(1) = 0 and f(n) = (f(n - 1) + k) % n; the winner is f(n) + 1 in 1-based numbering.
Precompute a boolean prime table in O(N log log N), crossing out multiples from p * p.
Halve the exponent each iteration: x^n = (x^2)^(n/2), times x when n is odd, in O(log N).
gcd(a, b) = gcd(b, a % b) until b == 0; fold it over the whole list.
cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x): positive is a left turn, zero is collinear.
Keep the i-th item (1-based) with probability 1 / i: replace the pick when randrange(i) == 0.
Set k -= 1 so k counts from 0. At each slot, every choice starts a block of the same size, block (fact[n - 1 - slot] for permutations): take choice number k // block, then k %= block is the rank inside that block. For the rank of a given item, add (unused choices before its digit) * block at each slot, then add 1.
Pattern Architecture & Invariant Design Masterclass
Mathematical and geometric problems model continuous spaces using discrete structures. Invariants such as 4-pointer inward contraction for spiral matrices, greatest common divisor (GCD) reduction for normalized slope vectors, and 32-bit overflow guard checks ensure exact simulation without floating-point drift.
The invariants to say out loud:
- 4-Boundary Box: "After traversing the top row and right column, I must verify
top <= bottomandleft <= rightbefore reversing direction back across bottom and left." - Coprime Slopes: "Never compare floating-point slopes. Reduce displacement vectors by
gcd(dx, dy)and store coprime integer pairs."
🔬 Variable Anatomy: The 7 Variables Every Math & Geometry Solution Uses
To connect abstract geometric invariants with concrete Python syntax, here is the universal code anatomy showing what every variable does:
import mathfrom collections import defaultdict def mathGeometryMasterTemplate( matrix: list[list[int]], points: list[list[int]], n: int) -> tuple[list[int], int, int]: # ========================================================================= # 1. THE 4-BOUNDARY COORDINATE POINTERS # ========================================================================= top, bottom = 0, len(matrix) - 1 # ↕️ Vertical Boundary Bounds left, right = 0, len(matrix[0]) - 1 # ↔️ Horizontal Boundary Bounds spiral_res = [] # ========================================================================= # 2. 4-POINTER PERIMETER CONTRACTION (SPIRAL MATRIX) # ========================================================================= while top <= bottom and left <= right: # Top row: left -> right for c in range(left, right + 1): spiral_res.append(matrix[top][c]) top += 1 # Right col: top -> bottom for r in range(top, bottom + 1): spiral_res.append(matrix[r][right]) right -= 1 # Bottom row: right -> left (Guarded against single row) if top <= bottom: for c in range(right, left - 1, -1): spiral_res.append(matrix[bottom][c]) bottom -= 1 # Left col: bottom -> top (Guarded against single col) if left <= right: for r in range(bottom, top - 1, -1): spiral_res.append(matrix[r][left]) left += 1 # ========================================================================= # 3. GCD REDUCED FRACTION SLOPE HASHING # ========================================================================= slope_counts = defaultdict(int) if points: x1, y1 = points[0] for x2, y2 in points[1:]: dx = x2 - x1 dy = y2 - y1 if dx == 0 and dy == 0: continue g = math.gcd(dx, dy) reduced_slope = (dy // g, dx // g) # ⚡ Coprime integer slope key if reduced_slope[0] < 0 or (reduced_slope[0] == 0 and reduced_slope[1] < 0): reduced_slope = (-reduced_slope[0], -reduced_slope[1]) slope_counts[reduced_slope] += 1 # ========================================================================= # 4. 32-BIT OVERFLOW GUARD & DIGIT EXTRACTION # ========================================================================= INT_MAX = 2**31 - 1 INT_MIN = -2**31 rev = 0 temp_n = n sign = -1 if temp_n < 0 else 1 temp_n = abs(temp_n) while temp_n > 0: digit = temp_n % 10 temp_n //= 10 # ⚠️ CRITICAL: Check overflow BEFORE multiplying if rev > (INT_MAX - digit) // 10: rev = 0 break rev = rev * 10 + digit reversed_int = sign * rev return spiral_res, max(slope_counts.values(), default=0), reversed_int Variable Role Reference:
| Variable | Role in Code | Question It Answers | ❌ What Happens If Omitted or Mismanaged? |
|---|---|---|---|
top / bottom | Row boundary pointers | "What is the active vertical range of unvisited cells?" | ❌ Without contracting top += 1 / bottom -= 1, rows are traversed multiple times. |
left / right | Col boundary pointers | "What is the active horizontal range of unvisited cells?" | ❌ Omitting if top <= bottom before bottom sweep repeats middle rows on non-square matrices. |
dx / dy | Coordinate delta | "What is the displacement between two geometric points?" | ❌ Subtracting in inconsistent order (x1 - x2) vs (x2 - x1) inverts vector direction. |
g (gcd) | Greatest common divisor | "What factor reduces (dy, dx) to its unique coprime ratio?" | ❌ Using floating-point division dy / dx introduces precision errors where distinct slopes collide in hash keys. |
INT_MAX | 32-bit clamp threshold | "Will rev * 10 + digit exceed 32-bit signed integer capacity?" | ❌ Checking overflow after multiplying triggers runtime integer wrap/overflow in fixed-width languages. |
digit | x % 10 | "What is the current least significant base-10 digit?" | ❌ In Python, negative % 10 returns positive; in C++/Java it returns negative, breaking parity. |
exp | Binary power exponent | "Can we halve the exponent to reduce power calculation to ?" | ❌ Iterating times for times out on large exponents (). |
🧠 The Universal Contract of Discrete Geometry:
- Coprime Slope Contract: Never hash
float(dy) / float(dx)due to IEEE 754 precision drift. Always reduce byg = gcd(dx, dy)and hash the integer tuple(dy // g, dx // g). Handle vertical lines(1, 0)and horizontal lines(0, 1)consistently.- Boundary Guard Invariant: When contracting a 2D matrix box, the horizontal and vertical sweeps shrink the space. Always verify
top <= bottomandleft <= rightbefore initiating the return bottom and left sweeps.
🌟 The 3 Dimensions of Math & Geometry & The Precision-Boundary Invariant
Every Math & Geometry problem is governed by 3 Fundamental Dimensions:
┌─────────────────────────────────────────────────────────────────────────────┐│ THE 3 DIMENSIONS OF MATH & GEOMETRY │├───────────────────┬───────────────────────────────────┬─────────────────────┤│ Dimension │ Mechanism in Code │ Question It Answers │├───────────────────┼───────────────────────────────────┼─────────────────────┤│ 1. BOUNDARY │ Monotonic inward contraction: │ "How does perimeter ││ CONTRACTION │ `top, bottom, left, right` │ geometry shrink?" │├───────────────────┼───────────────────────────────────┼─────────────────────┤│ 2. ARITHMETIC │ Coprime reduction `(dy//g, dx//g)`│ "How do we preserve ││ PRECISION │ Pre-multiplication overflow guard │ exact equality?" │├───────────────────┼───────────────────────────────────┼─────────────────────┤│ 3. NUMBER THEORY │ Binary Exponentiation: O(log N) │ "How do we avoid ││ REDUCTION │ Sieve: O(N log log N) │ O(N^2) brute force ││ │ instead of trial division │ operations?" │└───────────────────┴───────────────────────────────────┴─────────────────────┘ How this plays out in actual problems:
🟢 Scenario A: Perimeter Box Contraction (Spiral Matrix LC 54)
- Traversing a matrix in spiral order naively requires an visited grid.
- The Rule: Maintain 4 boundary limits (
top = 0, bottom = R-1, left = 0, right = C-1). Traverse top edge (L➔R) and incrementtop += 1. Traverse right edge (T➔B) and decrementright -= 1. Guard inner reverse sweeps withif top <= bottomandif left <= right. - The Result: Complete traversal in time with extra space.
🔵 Scenario B: Coprime Slope Hashing (Max Points on a Line LC 149)
- Checking if 3 points are collinear by comparing
(y2 - y1) / (x2 - x1) == (y3 - y1) / (x3 - x1)fails on vertical lines (divide-by-zero) and floats. - The Rule: Compute
g = math.gcd(dx, dy). Normalize signs sody > 0(ordy == 0anddx > 0). Store coprime pairs(dy // g, dx // g)in a hash map. - The Result: Guaranteed zero precision drift across all lines.
🟣 Scenario C: Binary Exponentiation (Pow(x, n) LC 50)
- Computing with a loop takes multiplications, which times out for .
- The Rule: Halve the exponent each step: if is even, ; if is odd, .
- The Result: Collapses operations to about 31 squarings plus one multiplication per 1-bit of (at most about 62 multiplications, ).
💡 The Ultimate Takeaway Decision Rule:
When analyzing a math or coordinate problem, ask: "Is this perimeter navigation, slope collinearity, or large arithmetic reduction?"
- If Matrix spiral / rotation ➔ 4-Pointer Perimeter Contraction ().
- If Line collinearity / slopes ➔ Coprime GCD tuple hashing
(dy//g, dx//g).- If Large powers or primes ➔ Binary Exponentiation () or Sieve ().
- If 32-bit digit operations ➔ Pre-multiplication overflow guard
rev > (INT_MAX - d) // 10.
🎯 The 4 Essential Math & Geometry Paradigms & Archetypes
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐│ THE 4 CORE MATH & GEOMETRY ARCHETYPES │├────────────────────────┬──────────────────────┬───────────────────────────────┬──────────────────────────────────┤│ Archetype │ Coordinate Mechanism │ Exact Arithmetic Invariant │ Target Problems & Complexity │├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 1. Perimeter Contract. │ 4 shrinking bounds │ Guard reverse sweeps with │ Spiral Matrix (LC 54), Spiral ││ (4-Boundary Box) │ `top, bottom, L, R` │ `top <= bottom & left <= right│ Matrix II (LC 59) — O(R * C) │├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 2. Coprime GCD Slopes │ Coordinate deltas │ `g = gcd(dx, dy)` yields │ Max Points on a Line (LC 149) ││ (Collinear Points) │ `(x2 - x1, y2 - y1)` │ normalized `(dy//g, dx//g)` │ — O(N^2) │├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 3. Overflow Surgery │ Base-10 extraction │ Check `rev > (MAX - d) // 10` │ Reverse Integer (LC 7), String ││ (Digit Reversal) │ `d = x % 10; x //= 10` before multiplying by 10 │ to Integer atoi (LC 8) — O(log N)│├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 4. Logarithmic Powers │ Binary Exponentiation│ $x^n = (x^2)^{n/2}$; │ Pow(x, n) (LC 50) — O(log N); ││ (Sieve & Pow) │ Sieve crossing $p^2$ │ cross multiples in sieve │ Count Primes (LC 204) — ││ │ │ │ O(N log log N) │└────────────────────────┴──────────────────────┴───────────────────────────────┴──────────────────────────────────┘ 🌀 1. 4-Pointer Perimeter Contraction (LC 54 — Spiral Matrix)
- Mental Model: Peels rectangular perimeters layer-by-layer like an onion.
- Code: see the Spiral Matrix card.
📐 2. Number Theory & GCD Slope Hashing (LC 149)
- Mental Model: Points share a line if their normalized slope vectors from an anchor point are identical.
- Code: see the Canonical Key card.
🔢 3. 32-Bit Integer Overflow Surgery (LC 7)
- Mental Model: Reverse digits one by one while verifying that multiplying by 10 will not overflow 32-bit signed integer limits.
- Code: see the Digit Manipulation card.
⚡ 4. Binary Exponentiation (LC 50 — Pow(x, n))
- Mental Model: Repeated squaring halves the computation size at each step.
- Code: see the Fast Power card.
🌳 Evaluating Mathematical Invariants, Modular Arithmetic & Precision Guarantees
Maintaining exactness in mathematical calculations requires understanding computer arithmetic:
1. In-Place Matrix Rotation (Transpose + Reflect)
- To rotate an matrix 90 degrees clockwise in-place with extra space:
- Transpose: Swap
matrix[i][j]withmatrix[j][i]for all . - Reflect: Reverse each row
row.reverse().
- Transpose: Swap
2. Guarding Modulo of Negative Numbers
- In languages like C++ and Java,
-7 % 3yields-1rather than2. To guarantee a non-negative residue, use(x % m + m) % m.
💡 Staff SWE Architectural Invariant:
Avoid floating-point types for discrete geometry comparisons. Always reduce coordinate ratios to coprime integer pairs with GCD to eliminate precision drift across platforms.
Understanding Invariants: Perimeter Contraction & Coprime Slope Precision
Discrete geometry problems eliminate floating-point precision loss by reducing coordinate deltas to coprime integer ratios using GCD, and compress matrix state via 4-pointer perimeter box contraction.
Contracting 4-boundary boxes (top, bottom, left, right) guarantees every cell in a 2D matrix is visited exactly once in O(1) auxiliary space without a visited hash set.
Contracts boundary pointers after top and right sweeps; without guards, non-square matrices re-traverse cells.
Prevents IEEE 754 floating-point drift and avoids divide-by-zero crashes on vertical lines.
Common Questions
"In Spiral Matrix, why do the bottom-row and left-column sweeps need an extra if top <= bottom / if left <= right guard, when the top-row and right-column sweeps don't need one?"
Because by the time execution reaches the third and fourth sides of a lap, the first two sides may have already shrunk the remaining region down to a single row or column — and without the guard, that same row or column gets walked a second time, in the opposite direction.
Each full lap peels one layer off the "onion": top row left-to-right (
top += 1), right column top-to-bottom (right -= 1), bottom row right-to-left (bottom -= 1), left column bottom-to-top (left += 1). The first two sides are always safe because the loop conditionwhile top <= bottom and left <= rightalready guaranteed a valid region existed before the lap began. But the first two sides' own pointer updates (top += 1,right -= 1) happen during the lap — so by the time you reach the third side,topmay have already passedbottom(this was a single remaining row), or by the fourth sideleftmay have already passedright(a single remaining column).Without the guard, the bottom-row sweep would re-walk the exact same row the top-row sweep just finished, in reverse — double-counting cells that were already collected. The two guard checks exist purely to detect "this layer has already been fully collapsed to a line by the earlier sides of this same lap" and skip the redundant sweep. It's specific to the third and fourth sides because only they can observe a collapse the first two sides just caused.
Coach discussion points
- 1.
How do we cleanly represent vertical line slopes where
dx == 0when using the GCD approach? - 2.
Explain how coordinate matrix rotation
matrix[i][j]maps tomatrix[j][n - 1 - i]for a 90-degree clock-wise turn. - 3.
Why must
dividend == INT_MIN and divisor == -1be checked before any other arithmetic in Divide Two Integers, rather than after computingabs(dividend)? - 4.
Walk through why doubling
tempandmultipletogether in lock-step guarantees the bit-shift doubling algorithm removes at least half of the remaining dividend on every outer loop iteration, bounding total iterations toO(log N). - 5.
In Integer to Roman, why does putting the subtractive forms (900=CM, 400=CD, 90=XC, 40=XL, 9=IX, 4=IV) directly into the ordered value/symbol table eliminate the need for any per-digit conditional branching?
- 6.
What would break if the Integer to Roman value/symbol table were not kept in strictly decreasing order?
- 7.
For reverse integer, why does Python let you cheat the 32-bit limit naturally, and how do you enforce it algorithmically in Python?
- 8.
In Set Matrix Zeroes, why may row 0 and column 0 hold the flags at all, and why must
first_row_zeroandfirst_col_zerobe recorded before the first flag is written and used only after the second pass? - 9.
In Convex Polygon, why must a turn of
0be skipped before it is compared withprevor stored in it, and which two turns does the% nwrap-around add? - 10.
In Find Greatest Common Divisor of Array, why do
(a, b)and(b, a % b)share every common divisor, and why musta, b = b, a % bupdate both values in one statement? - 11.
In Linked List Random Node, why does keeping the i-th node with chance exactly 1/i leave every node with chance 1/n at the end, and why must
icount the current node before the draw? - 12.
In Permutation Sequence, why do the arrangements that share a first digit form one block of
(n - 1)!, and why mustk -= 1come beforek // block?
Complexity analysis
- Spiral Matrix: Time, Space.
- Max Points: Time, Space.
- Reverse Integer: Time, Space.
- Josephus: Time, Space.
- Pow(x, n): Time, Space.
- Count Primes: Time, Space.
- Integer to Roman: Time, Space (fixed 13-entry table).
- Divide Two Integers: Time, Space.
- Set Matrix Zeroes: Time, Space (flags in row 0 and column 0).
- Convex Polygon: Time, Space (one cross product per corner).
- Find Greatest Common Divisor of Array: Time, Space (Euclid's remainder steps on the two ends).
- Linked List Random Node: Time per
getRandom, Space (reservoir sampling, one walk per pick). - Permutation Sequence: Time, Space (Count and Skip:
Nslots, each with anO(N)pop).
Graphics engines frequently rotate textures or traverse grids spirally to encode image blocks. Collinear points detection is used in computer vision for Hough transforms to detect lines. Overflow checks are critical for low-level banking applications where integer maximums can wrap around to negative numbers, causing catastrophic faults.
💡 Don't worry if modular arithmetic or geometric slope hashing feels mathematical right now! Mathematical algorithms exploit algebraic identities and coordinate transformations to avoid exhaustive simulation. As you progress through the Archetype Variants (from Count Primes Sieve and Binary Exponentiation to Spiral Matrix and Max Points on a Line), each hands-on challenge will cement this intuition until guarding integer overflow and applying invariant formulas becomes second nature.
Traversing bottom row or left column when boundaries have crossed: After traversing right and down,
topandbottomorleftandrightmay have crossed (e.g. in single-row or single-column matrices). You must verifytop <= bottombefore traversing left, andleft <= rightbefore traversing up.Allocating visited matrix when 4 boundaries suffice: Maintaining an visited boolean matrix wastes auxiliary space. Four bounding pointers (
top, bottom, left, right) achieve auxiliary space.Off-by-one on boundary increments: Forgetting to advance boundaries (
top += 1,right -= 1,bottom -= 1,left += 1) causes the traversal to repeat the outer perimeter infinitely.
Staff+ Systems Architecture & Hardware Considerations
Triggers: "greatest common divisor (GCD)", "prime factorization / sieve", "matrix rotation / spiral traversal", "combinatorics and modular arithmetic". Algebraic equations, modular arithmetic, and discrete coordinate transformations.
Values up to or matrix dimensions up to . Brute-force simulation times out; mathematical closed forms or Euclidean algorithm drop complexity from to .
Integer overflow during multiplication prior to modulo; division in modular arithmetic requiring Fermat's Little Theorem / modular inverse; matrix boundary off-by-one errors during spiral traversals.
Conceptual Foundation & Intuitive Analogy
🧭 Conceptual Foundation & Pattern Intuition
Traversing an 2D matrix in spiral order is not about calculating complex geometric curves or calculating trigonometric trajectories. It is an exercise in Rectangular Boundary Contraction. Four boundary walls—top, bottom, left, right—define the unvisited rectangular domain. Traversal proceeds along the active perimeter, and as each side is completed, its boundary wall is shifted inward monotonically.
🏟️ The Analogy: Peeling an Onion, Layer by Layer
Imagine peeling an onion with flat, rectangular layers. You slice off the top outer skin (traverse left to right), slice off the right skin (traverse top to bottom), shave off the bottom skin (traverse right to left), and finally peel up the left skin (traverse bottom to top). What remains inside is another smaller, pristine rectangular onion. You repeat this peeling cycle until nothing remains.
🔑 Why It Works
Notice the crucial safety checks if top <= bottom and if left <= right before the backward passes! For non-square matrices (e.g., or ), the first two sweeps contract the boundary past the opposing wall. Without these guards, the backward loops would re-traverse already visited cells, creating duplicate elements.
Boundary pointers (top, bottom, left, right) contract inward monotonically after each directional sweep.
Boundary pointers contract inward after each directional sweep; modular arithmetic bounds state cyclically.
while top <= bottom and left <= right: sweep right, down, left, up, contracting respective pointer.
The 11 Mental Models of Math and Geometry
Categorize math problems by their spatial or arithmetic invariant before writing code.
1Digit Manipulation
integer x ➔ digit modulo extraction ➔ overflow clampInteger Overflow Handling & Digit Manipulation
| 1 | def reverse_integer(x: int) -> int: |
| 2 | """Reverses digits of a 32-bit signed integer with overflow detection. |
| 3 | Time: O(log10 |x|) | Space: O(1) |
| 4 | """ |
| 5 | INT_MAX = 2**31 - 1 # 2147483647 |
| 6 | INT_MIN = -2**31 # -2147483648 |
| 7 | |
| 8 | # 1. Normalize the Sign: |
| 9 | # Python's % and // round toward minus infinity, so working on a |
| 10 | # negative x directly would misbehave; the sign is split off and reapplied at the end. |
| 11 | sign = -1 if x < 0 else 1 |
| 12 | x = abs(x) |
| 13 | res = 0 # digits of x, read from the right, rebuilt in reverse order |
| 14 | |
| 15 | # 2. Peel Off and Append One Digit at a Time: |
| 16 | # x % 10 reads the last digit and x //= 10 drops it; appending that |
| 17 | # digit to res with res * 10 + digit builds the reversed number one digit per step. |
| 18 | while x > 0: |
| 19 | digit = x % 10 |
| 20 | x //= 10 |
| 21 | |
| 22 | # 3. Detect Overflow Before It Happens: |
| 23 | # res * 10 + digit must stay within INT_MAX; checking res against |
| 24 | # INT_MAX // 10 first catches the overflow before the multiply can occur. |
| 25 | if res > INT_MAX // 10 or (res == INT_MAX // 10 and digit > 7): |
| 26 | return 0 |
| 27 | |
| 28 | res = res * 10 + digit |
| 29 | |
| 30 | # 4. Reapply the Sign and the Final Range Check: |
| 31 | # The sign set aside in step 1 is multiplied back in, then the result |
| 32 | # is re-checked against the full signed range in case that flipped it out of bounds. |
| 33 | res *= sign |
| 34 | return res if INT_MIN <= res <= INT_MAX else 0 |
Treat a number as a row of digits: x % 10 reads the last digit, x //= 10 drops it, and res = res * 10 + digit appends a digit on the right, so any number can be taken apart and rebuilt one digit at a time without turning it into a string. Each step moves exactly one digit, so reading from the right while appending builds the digits in reverse order, as reverse_integer does; the same two moves, in other orders, compare a number with its mirror, add numbers stored as digit arrays or parse text into a number. Work on abs(x) and put the sign back at the end, because in Python % and // round toward minus infinity for negatives (-7 % 10 == 3). The trap is overflow: test res > INT_MAX // 10 (or res == INT_MAX // 10 and digit > 7) before res * 10 + digit, because with fixed-width integers the multiply itself overflows: Java silently wraps the value around, and in C++ signed overflow is undefined behavior.
Gotcha 1 of 3
- The trap
Gotcha 1: Checking the 32-bit limit after
res * 10 + digitMistakeres = res * 10 + digit if res > INT_MAX: return 0Fixif res > INT_MAX // 10 or (res == INT_MAX // 10 and digit > 7): return 0 res = res * 10 + digitWhat happens: In Java or C++,
resis a 32-bit integer.res * 10 + digitcan go pastINT_MAX, which is 2^31 - 1. Java then wraps the value around, and in C++ signed overflow is undefined behavior. A check after the multiply only sees the wrapped value. For x = 1534236469, the true reverse is 9646324351, and Java's wrapped value is 1056389759. That value is belowINT_MAX, so the late check lets it through. Python ints never overflow, so in Python both orders return 0. The check before the multiply is the one that works in every language.
2Spiral Matrix
grid (R x C) ➔ top/bottom/left/right ➔ inward spiralSpiral Matrix Traversal (Layer-by-Layer Boundary Shrinking)
| 1 | def spiral_order(matrix: list[list[int]]) -> list[int]: |
| 2 | """Spiral Matrix: read the matrix ring by ring with four boundaries. |
| 3 | Time: O(R * C) | Space: O(1) auxiliary |
| 4 | """ |
| 5 | if not matrix or not matrix[0]: |
| 6 | return [] |
| 7 | |
| 8 | top, bottom = 0, len(matrix) - 1 |
| 9 | left, right = 0, len(matrix[0]) - 1 |
| 10 | result: list[int] = [] |
| 11 | |
| 12 | while top <= bottom and left <= right: |
| 13 | # 1. Sweep Top (Left -> Right) |
| 14 | for c in range(left, right + 1): |
| 15 | result.append(matrix[top][c]) |
| 16 | top += 1 |
| 17 | |
| 18 | # 2. Sweep Right (Top -> Bottom) |
| 19 | for r in range(top, bottom + 1): |
| 20 | result.append(matrix[r][right]) |
| 21 | right -= 1 |
| 22 | |
| 23 | # 3. Sweep Bottom (Right -> Left) with Guard! |
| 24 | if top <= bottom: |
| 25 | for c in range(right, left - 1, -1): |
| 26 | result.append(matrix[bottom][c]) |
| 27 | bottom -= 1 |
| 28 | |
| 29 | # 4. Sweep Left (Bottom -> Top) with Guard! |
| 30 | if left <= right: |
| 31 | for r in range(bottom, top - 1, -1): |
| 32 | result.append(matrix[r][left]) |
| 33 | left += 1 |
| 34 | |
| 35 | return result |
Walk the matrix one ring at a time with four boundaries, top, bottom, left and right, that close in from all four sides like converging pointers, and move each boundary inward right after its side is read. Each boundary marks the first row or column not yet read on its side, so after the four sweeps the unread cells form a smaller rectangle and the same sweeps repeat on it. The trap is the last, thin ring: after the top and right sweeps there may be no row or no column left, so guard the bottom sweep with if top <= bottom and the left sweep with if left <= right, or a single row such as [[1,2,3]] returns [1,2,3,2,1].
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out the
if top <= bottomguardMistakefor c in range(right, left - 1, -1): result.append(matrix[bottom][c]) bottom -= 1Fixif top <= bottom: for c in range(right, left - 1, -1): result.append(matrix[bottom][c]) bottom -= 1What happens: After the top sweep and the right sweep, the rows may be used up. For
[[1,2,3]], the top sweep reads row 0 and setstopto 1, whilebottomis 0. Without the guard, the bottom sweep reads row 0 again, backwards. The left sweep needsif left <= rightfor the same reason.Example: matrix = [[1, 2, 3]]Mistake: returns [1, 2, 3, 2, 1]Fix: returns [1, 2, 3]
3Fast Power
base x, exponent n ➔ halve exponent repeatedly ➔ O(log N) powerFast Power (Exponentiation by Squaring)
| 1 | def my_pow(x: float, n: int) -> float: |
| 2 | """Binary Exponentiation: computes x^n by squaring. |
| 3 | Time: O(log N) | Space: O(1) |
| 4 | """ |
| 5 | if n < 0: |
| 6 | x = 1.0 / x # x^-n == (1/x)^n |
| 7 | n = -n |
| 8 | |
| 9 | result = 1.0 |
| 10 | curr_product = x # Holds x^(2^i) for the current bit i |
| 11 | |
| 12 | while n > 0: |
| 13 | if n % 2 == 1: |
| 14 | result *= curr_product # Bit is set: multiply this power in |
| 15 | curr_product *= curr_product # Square for the next bit |
| 16 | n //= 2 # Move to the next bit |
| 17 | |
| 18 | return result |
Write n in binary: x^13 = x^8 · x^4 · x^1, so only the powers x^1, x^2, x^4, ... are ever needed. curr_product squares once per bit, and result takes it in only when the current bit of n is 1 (n % 2 == 1). It is correct because result * curr_product ** n stays equal to the original x ** n every round, and the loop ends with n = 0. The trap is a negative n: flip first with x = 1 / x and n = -n (in Java or C++ copy n into 64 bits first, since -n overflows for -231).
Gotcha 1 of 4
- The trap
Gotcha 1: Negating
nwithout replacingxby1 / xMistakeif n < 0: n = -nFixif n < 0: x = 1.0 / x n = -nWhat happens: For a negative
n,x ** nequals(1 / x) ** -n. The loop only handlesn >= 0. Withn = -nalone, it computesx ** -n. Somy_pow(2.0, -2)returns 4.0 instead of 0.25.Example: x = 2.0, n = -2Mistake: returns 4.0Fix: returns 0.25
4In-Place Marking
matrix ➔ markers in the first row / column or spare bits ➔ apply in a second passIn-Place Markers (First Row/Column Flags, Two States per Cell)
O(1) extra memory, but each change depends on the input as it was: rows or columns to wipe, or a new state that every neighbour must still read in its old form.| 1 | def set_zeroes(matrix: list[list[int]]) -> None: |
| 2 | """Set Matrix Zeroes: row 0 and column 0 hold the flags for the other rows and columns. |
| 3 | Time: O(M * N) | Space: O(1) |
| 4 | """ |
| 5 | m, n = len(matrix), len(matrix[0]) |
| 6 | |
| 7 | # 1. Save Row 0's and Column 0's Own Zeros First: |
| 8 | # Pass 1 below overwrites row 0 and column 0 with flags, so whether |
| 9 | # they originally held a zero must be recorded before any flag is written. |
| 10 | first_row_zero = any(matrix[0][c] == 0 for c in range(n)) |
| 11 | first_col_zero = any(matrix[r][0] == 0 for r in range(m)) |
| 12 | |
| 13 | # 2. Mark Flags in Row 0 and Column 0: |
| 14 | # A zero found inside the matrix (outside row 0 and column 0) means its |
| 15 | # whole row and column end up zero, so that is recorded by zeroing the |
| 16 | # row's cell in column 0 and the column's cell in row 0. |
| 17 | for r in range(1, m): |
| 18 | for c in range(1, n): |
| 19 | if matrix[r][c] == 0: |
| 20 | matrix[r][0] = 0 |
| 21 | matrix[0][c] = 0 |
| 22 | |
| 23 | # 3. Apply the Flags to Every Inner Cell: |
| 24 | # Any inner cell whose row flag or column flag is set is zeroed; row 0 |
| 25 | # and column 0 themselves are skipped here because they still need to be read. |
| 26 | for r in range(1, m): |
| 27 | for c in range(1, n): |
| 28 | if matrix[r][0] == 0 or matrix[0][c] == 0: |
| 29 | matrix[r][c] = 0 |
| 30 | |
| 31 | # 4. Clear Row 0 and Column 0 Last: |
| 32 | # Row 0 and column 0 are cleared only after every other cell has read |
| 33 | # their flags; clearing them earlier would turn every flag true and wipe the whole matrix. |
| 34 | if first_row_zero: |
| 35 | for c in range(n): |
| 36 | matrix[0][c] = 0 |
| 37 | if first_col_zero: |
| 38 | for r in range(m): |
| 39 | matrix[r][0] = 0 |
When no extra memory is allowed, store the marks in cells whose final value you already know: a zero at (r, c) means row r and column c end up all zero, so pass 1 writes the flags matrix[r][0] = 0 and matrix[0][c] = 0, and pass 2 zeroes every cell whose row flag or column flag is 0. Writing a flag loses nothing, because both flag cells become zero in the answer anyway; the same idea packs two states into one cell when every neighbour must still read it in its old form, because the old value stays readable until every reader is done, whether that means a whole cell repurposed as a flag or one bit of it holding the next state. The trap is that row 0 and column 0 are data as well as flags: compute first_row_zero and first_col_zero before pass 1 writes any flag, keep both passes to r >= 1 and c >= 1, and clear row 0 and column 0 last, because clearing them first turns every flag to 0 and wipes the whole matrix.
Gotcha 1 of 5
- The trap
Gotcha 1: Clearing row 0 before pass 2 reads the flags
Mistakeif first_row_zero: for c in range(n): matrix[0][c] = 0 for r in range(1, m): for c in range(1, n): if matrix[r][0] == 0 or matrix[0][c] == 0: matrix[r][c] = 0Fixfor r in range(1, m): for c in range(1, n): if matrix[r][0] == 0 or matrix[0][c] == 0: matrix[r][c] = 0 if first_row_zero: for c in range(n): matrix[0][c] = 0What happens: After pass 1, row 0 holds the column flags. A 0 at
matrix[0][c]means "clear columnc". If row 0 is cleared before pass 2, everymatrix[0][c]becomes 0. Pass 2 then finds a flag in every column and clears the whole matrix.Example: matrix = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]Mistake: returns [[0, 0, 0], [0, 0, 0], [0, 0, 0]]Fix: returns [[0, 0, 0], [0, 4, 5], [0, 7, 8]]
5Cross Product
three points ➔ sign of the cross product ➔ left turn, right turn or straight lineCross Product Orientation Test (Turn Direction, Collinearity, Convexity)
| 1 | def cross(o: list[int], a: list[int], b: list[int]) -> int: |
| 2 | """Orientation test: > 0 when o -> a -> b turns left, < 0 when it turns right, 0 when the points are collinear.""" |
| 3 | # cross is twice the signed area of triangle o, a, b: its sign alone gives the turn direction. |
| 4 | return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) |
| 5 | |
| 6 | |
| 7 | def is_convex(points: list[list[int]]) -> bool: |
| 8 | """Convex Polygon: every turn along the boundary has the same cross-product sign. |
| 9 | Time: O(N) | Space: O(1) |
| 10 | """ |
| 11 | n = len(points) |
| 12 | prev = 0 # prev: the last non-zero turn seen so far; 0 means no turn has been seen yet |
| 13 | for i in range(n): |
| 14 | # 1. Compute the Turn at the Next Corner: |
| 15 | # cross() gives the sign of the turn o -> a -> b in one integer; % n wraps the index, so the |
| 16 | # two turns that close the polygon are checked in the same pass as every other corner. |
| 17 | turn = cross(points[i], points[(i + 1) % n], points[(i + 2) % n]) |
| 18 | |
| 19 | # 2. Classify the Turn (the trap): |
| 20 | # A turn of 0 is a straight corner: it is allowed and tells nothing about direction, so it |
| 21 | # is skipped before prev is touched. A later non-zero turn with the opposite sign means the |
| 22 | # boundary bent back on itself, a dent, so the polygon is not convex. |
| 23 | if turn == 0: |
| 24 | continue |
| 25 | if prev != 0 and (turn > 0) != (prev > 0): |
| 26 | return False |
| 27 | |
| 28 | # 3. Remember the Direction: |
| 29 | # Save this non-zero turn so the next corner's turn can be compared against it. |
| 30 | prev = turn |
| 31 | return True |
The cross product cross(o, a, b) = (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) answers "which way does the path o -> a -> b turn?" with one integer: positive for a left (counter-clockwise) turn, negative for a right turn, 0 when the three points lie on one line. A polygon is convex exactly when its boundary always turns the same way, so is_convex computes turn at every corner, wrapping around with % n so the two turns that close the loop are checked too, and returns False the first time a turn's sign differs from prev. This is correct because the value is twice the signed area of the triangle o, a, b: its sign flips exactly when b crosses from one side of the line through o and a to the other, and it uses only integer multiplication and subtraction, so there is no division by zero on a vertical edge and no rounding error. The trap is a turn of 0: three points in a row on one line make a straight corner, which is allowed and says nothing about the direction, so continue past it. Treating it as a change of direction rejects convex polygons that have extra points along an edge, and storing it in prev overwrites the record of which way the boundary was turning, so a dent right after a straight corner slips through.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
if turn == 0: continueMistakeif prev != 0 and (turn > 0) != (prev > 0):Fixif turn == 0: continue if prev != 0 and (turn > 0) != (prev > 0):What happens: A turn of 0 is a straight corner. It is allowed, and it says nothing about the direction. Without
continue, the 0 reaches the sign test. After a right turn, the test passes, andprev = turnsetsprevto 0. The last known direction is lost. A dent right after the straight corner is then not caught. After a left turn, the same 0 fails the test, so a convex polygon is rejected.Example: points = [[6,6],[8,12],[12,12],[12,0],[0,0],[0,12],[4,12],[5,9]]Mistake: returns TrueFix: returns False
6Euclid's GCD
two numbers ➔ replace (a, b) by (b, a % b) ➔ greatest common divisorEuclidean Algorithm (Greatest Common Divisor)
| 1 | def gcd(a: int, b: int) -> int: |
| 2 | """Euclid's algorithm: gcd(a, b) == gcd(b, a % b), and gcd(a, 0) == a.""" |
| 3 | # 1. Replace the Pair Until It Shrinks to Zero (the trap): |
| 4 | # a % b is a minus a multiple of b, so a number dividing both a and b also divides a % b, and a |
| 5 | # number dividing b and a % b also divides a: both values must update from the old pair at once. |
| 6 | while b: |
| 7 | a, b = b, a % b |
| 8 | # 2. Return the Answer: |
| 9 | # The loop stops once b is 0; since gcd(a, 0) == a, a now holds the greatest common divisor. |
| 10 | return a |
| 11 | |
| 12 | |
| 13 | def find_gcd(nums: list[int]) -> int: |
| 14 | """Find Greatest Common Divisor of Array: Euclid's algorithm on the smallest and largest value. |
| 15 | Time: O(N + log M) | Space: O(1) |
| 16 | """ |
| 17 | lo, hi = min(nums), max(nums) # lo, hi: the two ends; only they matter, the other values play no part |
| 18 | return gcd(hi, lo) |
Euclid's algorithm finds the greatest common divisor of two numbers by replacing the pair (a, b) with (b, a % b) until b is 0; then a is the answer. Here find_gcd only needs the smallest and largest value, lo and hi, so it returns gcd(hi, lo). It is correct because a % b is a minus a multiple of b: any number that divides both a and b also divides a % b, and any number that divides b and a % b also divides a, so the pair changes but its common divisors never do. The trap is the update: write a, b = b, a % b as one statement. Updating a = b first makes the next line compute b % b, which is always 0, so the loop stops after one step and returns the smaller number.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
a = band thenb = a % bMistakea = b b = a % bFixa, b = b, a % bWhat happens: After
a = b, the oldais gone. The next line computesb % b, which is always 0. The loop stops after one step and returns the oldb. Sogcd(8, 3)returns 3 instead of 1.Example: a = 8, b = 3Mistake: returns 3Fix: returns 1
7Canonical Key
integers (a, b) ➔ Euclidean reduction ➔ normalized coprime factorsCanonical-Form Hashing (Normalize Before You Count)
| 1 | import math |
| 2 | from collections import defaultdict |
| 3 | |
| 4 | def max_points_on_line(points: list[list[int]]) -> int: |
| 5 | """Maximum points collinear on a plane using GCD slope hashing. |
| 6 | Time: O(N^2) | Space: O(N) |
| 7 | """ |
| 8 | # 1. Handle Trivial Inputs: |
| 9 | # Any 0, 1, or 2 points are always collinear, so the general counting |
| 10 | # below is skipped for those sizes. |
| 11 | if len(points) <= 2: |
| 12 | return len(points) |
| 13 | |
| 14 | max_pts = 0 # largest count of collinear points found across all anchors |
| 15 | |
| 16 | # 2. Anchor on Each Point in Turn: |
| 17 | # slopes counts, for the current anchor points[i], how many later points |
| 18 | # share each direction from it; a fresh dict per anchor keeps directions |
| 19 | # from different anchors from being mixed into the same count. |
| 20 | for i in range(len(points)): |
| 21 | slopes: dict[tuple[int, int], int] = defaultdict(int) |
| 22 | x1, y1 = points[i] |
| 23 | |
| 24 | for j in range(i + 1, len(points)): |
| 25 | x2, y2 = points[j] |
| 26 | dx, dy = x2 - x1, y2 - y1 |
| 27 | g = math.gcd(dx, dy) |
| 28 | |
| 29 | # 3. Reduce the Direction to a Canonical Key: |
| 30 | # Dividing dx and dy by their gcd keeps the same direction in |
| 31 | # lowest terms, then flipping the sign so dy is positive (or dy |
| 32 | # == 0 with dx positive) makes opposite vectors of one line hash alike. |
| 33 | slope = (dy // g, dx // g) |
| 34 | if slope[0] < 0 or (slope[0] == 0 and slope[1] < 0): |
| 35 | slope = (-slope[0], -slope[1]) |
| 36 | |
| 37 | # 4. Count and Track the Best Line Through This Anchor: |
| 38 | # Every point sharing a key lies on the same line through |
| 39 | # points[i]; the + 1 below counts the anchor point itself. |
| 40 | slopes[slope] += 1 |
| 41 | max_pts = max(max_pts, slopes[slope] + 1) |
| 42 | |
| 43 | return max_pts |
Before counting, turn each item into a key that is equal exactly when the items should count as equal; here the item is the direction from points[i] to points[j], and its key is (dy // g, dx // g) with g = gcd(dx, dy), sign fixed. Two directions lie on the same line through points[i] exactly when their reduced integer pairs match, and integers compare exactly, so two different directions never share one bucket, as rounded float slopes can when the coordinates are large. The trap is the sign: (1, -2) and (-1, 2) are the same direction, so force dy positive (or dy == 0 with dx positive) before counting slope, or one line is counted as two.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving the sign of
(dy // g, dx // g)unfixedMistakeslope = (dy // g, dx // g)Fixslope = (dy // g, dx // g) if slope[0] < 0 or (slope[0] == 0 and slope[1] < 0): slope = (-slope[0], -slope[1])What happens:
math.gcdnever returns a negative number. Sody // ganddx // gkeep the signs ofdyanddx. Forpoints = [[0, 0], [1, 1], [-1, -1]], the anchor[0, 0]gives[1, 1]the key(1, 1). It gives[-1, -1]the key(-1, -1), although both points lie on the same line. That one line is then counted in two buckets. The fix flips the pair so thatdyis positive, ordy == 0anddxis positive.Example: points = [[0, 0], [1, 1], [-1, -1]]Mistake: returns 2Fix: returns 3
8Count and Skip
rank k in a huge list in dictionary order ➔ at each slot, skip k // block blocks of equal size (k counted from 0) and keep k % block ➔ the k-th item, built one slot at a timeK-th in Dictionary Order by Block Counting (Unranking, Factorial Number System)
k-th item of a huge list sorted in dictionary order (the k-th permutation, route, string or number), or the rank of a given item in that list, and the list is far too long to build: k can reach n!, C(30, 15) or 1015.| 1 | def get_permutation(n: int, k: int) -> str: |
| 2 | """Permutation Sequence: skip whole blocks of arrangements that share the digits chosen so far. |
| 3 | Time: O(N^2) | Space: O(N) |
| 4 | """ |
| 5 | fact = [1] * n # fact[m] = m! |
| 6 | |
| 7 | # 1. Precompute Factorials: |
| 8 | # fact[m] holds m!, the number of arrangements of m items, needed below |
| 9 | # to size each slot's blocks without computing a factorial inside the main loop. |
| 10 | for m in range(1, n): |
| 11 | fact[m] = fact[m - 1] * m |
| 12 | |
| 13 | pool = [str(d) for d in range(1, n + 1)] # unused digits, smallest first |
| 14 | |
| 15 | # 2. Convert the Rank to 0-Based: |
| 16 | # k counts from 1, but k // block below needs the count of arrangements |
| 17 | # that come strictly before the answer, which is k - 1. |
| 18 | k -= 1 |
| 19 | result = [] # digits of the answer, chosen one slot at a time |
| 20 | |
| 21 | # 3. Choose Each Slot by Skipping Whole Blocks: |
| 22 | # Every unused digit starts a block of fact[n - 1 - slot] arrangements |
| 23 | # that share the digits chosen so far; dividing k by that block size |
| 24 | # finds how many whole blocks come before the answer, and the remainder |
| 25 | # becomes the answer's rank inside the chosen block for the next slot. |
| 26 | for slot in range(n): |
| 27 | block = fact[n - 1 - slot] |
| 28 | index = k // block |
| 29 | k %= block |
| 30 | result.append(pool.pop(index)) |
| 31 | return "".join(result) |
Items in dictionary order come in blocks: every arrangement that starts with the same digit sits together, and here every block at a slot holds the same number of arrangements, block = fact[n - 1 - slot]. So the answer's digit for a slot is found without listing anything: skip index = k // block whole blocks, take pool.pop(index), and continue inside the chosen block with k %= block, one slot to the right and one factorial smaller. This is correct because the blocks come in the same order as their first digits and all have the same size, so exactly index of them lie before the answer, and k % block is the answer's rank inside its own block; removing the used digit from pool keeps the next slot's choices in order. The trap is the rank's base: k counts from 1, so subtract 1 first. Without k -= 1, every k that is a multiple of block picks one block too far: n = 3, k = 3 gives "231" instead of "213".
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
k -= 1Mistakeresult = []Fixk -= 1 result = []What happens:
kcounts from 1, butk // blockneeds the number of arrangements before the answer. That number isk - 1. Withoutk -= 1, everykthat is a multiple ofblockpicks one block too far.Example: n = 3, k = 3Mistake: returns '231'Fix: returns '213'
9Josephus Problem
circle of n friends ➔ smaller circle's winner ➔ shift by kJosephus Problem (Circular Elimination Recurrence)
| 1 | def find_the_winner(n: int, k: int) -> int: |
| 2 | """Find the Winner of the Circular Game (Josephus): f(size) = (f(size - 1) + k) % size. |
| 3 | Time: O(N) | Space: O(1) |
| 4 | """ |
| 5 | winner = 0 # 0-indexed winner of the game with 1 friend |
| 6 | for size in range(2, n + 1): |
| 7 | winner = (winner + k) % size # Shift the smaller game's winner k seats |
| 8 | return winner + 1 # Friends are numbered from 1 |
Once the first friend leaves, the rest is the same game with one friend fewer, only started k seats later, so if winner is the answer for size - 1 friends, (winner + k) % size is the answer for size friends, and a loop from size = 2 up to n builds it without simulating anyone leaving. It is correct because removing the k-th friend and restarting from the next seat is exactly a shift of the smaller circle by k seats, and % size wraps that shift around the larger circle. The trap is the index base: the rule counts seats from 0, so start from winner = 0 for one friend and add 1 only in the final return, because the friends are numbered from 1.
Gotcha 1 of 4
- The trap
Gotcha 1: Returning
winnerwithout adding 1Mistakereturn winnerFixreturn winner + 1What happens: The recurrence counts seats from 0. So
winner = 0is the only seat when one friend is left. Find the Winner of the Circular Game (LC 1823) numbers the friends from 1. Only the finalreturnadds 1.Example: n = 5, k = 2Mistake: returns 2Fix: returns 3
10Sieve of Eratosthenes
upper bound n ➔ boolean sieve array ➔ prime countSieve of Eratosthenes
| 1 | def count_primes(n: int) -> int: |
| 2 | """Sieve of Eratosthenes: counts primes strictly below n. |
| 3 | Time: O(N log log N) | Space: O(N) |
| 4 | """ |
| 5 | if n <= 2: |
| 6 | return 0 # No primes below 2 |
| 7 | |
| 8 | is_prime = [True] * n |
| 9 | is_prime[0] = is_prime[1] = False # 0 and 1 are not prime |
| 10 | |
| 11 | for p in range(2, int(n ** 0.5) + 1): # Factors above sqrt(n) were already crossed out |
| 12 | if is_prime[p]: |
| 13 | for multiple in range(p * p, n, p): # Start at p*p: smaller multiples have a smaller factor |
| 14 | is_prime[multiple] = False |
| 15 | |
| 16 | return sum(is_prime) |
A number is composite exactly when it has a prime factor no larger than its square root, so cross off multiples only for primes p up to sqrt(n). Start each crossing at p * p: every smaller multiple p * k with k < p was already crossed off by a smaller prime factor of k. Whatever is still True in is_prime is prime. The trap is 0 and 1: they start True like every slot, but no crossing ever reaches them, so set both to False first or the count comes out 2 too high.
Gotcha 1 of 5
- The trap
Gotcha 1: Leaving
is_prime[0]andis_prime[1]as TrueMistakeis_prime = [True] * nFixis_prime = [True] * n is_prime[0] = is_prime[1] = FalseWhat happens: Every slot starts True. Crossing out starts at
p * p, which is at least 4. So no step ever reaches 0 or 1. Without the line,sum(is_prime)counts 0 and 1 as primes, and the count is 2 too high.Example: n = 10Mistake: returns 6Fix: returns 4
11Reservoir Sampling
stream of items ➔ keep each with chance 1/i ➔ one uniform random itemReservoir Sampling & Randomized Pick
| 1 | import random |
| 2 | from typing import Optional |
| 3 | |
| 4 | |
| 5 | class ListNode: |
| 6 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 7 | self.val = val |
| 8 | self.next = next |
| 9 | |
| 10 | |
| 11 | class Solution: |
| 12 | """Linked List Random Node: reservoir sampling, one pass per pick and O(1) extra space. |
| 13 | Time: O(N) per getRandom | Space: O(1) |
| 14 | """ |
| 15 | |
| 16 | def __init__(self, head: Optional[ListNode]): |
| 17 | self.head = head # Keep only the head: every pick walks the list again |
| 18 | |
| 19 | def getRandom(self) -> int: |
| 20 | result, i = 0, 0 # result: the current pick; i: nodes seen so far |
| 21 | node = self.head |
| 22 | while node: |
| 23 | # 1. Count This Node: |
| 24 | # i must count node before the draw, so the first node is always taken (randrange(1) is 0). |
| 25 | i += 1 |
| 26 | |
| 27 | # 2. Draw With Chance 1/i: |
| 28 | # randrange(i) is one of 0..i - 1, so it lands on 0 with chance exactly 1/i; every earlier |
| 29 | # node then keeps its own share, since it survives this draw with chance (i - 1)/i. |
| 30 | if random.randrange(i) == 0: |
| 31 | result = node.val |
| 32 | |
| 33 | # 3. Move to the Next Node: |
| 34 | # The same draw repeats at every node until the whole list has been walked once. |
| 35 | node = node.next |
| 36 | # 4. Return the Last Survivor: |
| 37 | # After the last node every node has had chance exactly 1/n, with no copy of the list ever made. |
| 38 | return result |
Reservoir sampling picks a uniform random node in one pass without knowing the length: walk the list, count the nodes with i, and let the i-th node replace result with chance exactly 1/i, drawn as random.randrange(i) == 0. It is fair because the shares stay equal at every step: the new node gets in with 1/i, and each earlier node, which was result with chance 1/(i - 1), survives the draw with (i - 1)/i, which leaves it at 1/i too; after the last node every node has chance 1/n, with no copy of the list. The trap is the chance: i must count the current node before the draw, so the first node is always taken (randrange(1) is 0). randrange(i + 1), a chance of 1/(i + 1), keeps the head only half the time and can leave result at its starting 0, and returning at the first successful draw always returns the head.
Gotcha 1 of 3
- The trap
Gotcha 1: Drawing
randrange(i + 1)instead ofrandrange(i)Mistakeif random.randrange(i + 1) == 0:Fixif random.randrange(i) == 0:What happens:
i += 1already counts the current node, so nodeimust win with chance 1/i. The head then wins with chance 1/1, so it is always kept first. Withrandrange(i + 1), the head wins only half the time. For the list [1, 2], each of 0, 1 and 2 is returned in one third of the calls. The 0 isresult's starting value, which is not in the list.Example: head = [1, 2], the set of values from 100 callsMistake: returns [0, 1, 2]Fix: returns [1, 2]
The Math & Geometry Decision Rule
When facing an unseen mathematical or geometric problem, ask in this order:
Validate bounds before res = res * 10 + digit.
Build the key first, e.g. (dy // g, dx // g) with g = gcd(dx, dy) and one fixed sign, then count keys in a dict.
Record the changes in the first row and column (or in spare bits of each cell), then apply them in a second pass.
Establish top, bottom, left, right bounds and contract clockwise.
f(1) = 0 and f(n) = (f(n - 1) + k) % n; the winner is f(n) + 1 in 1-based numbering.
Precompute a boolean prime table in O(N log log N), crossing out multiples from p * p.
Halve the exponent each iteration: x^n = (x^2)^(n/2), times x when n is odd, in O(log N).
gcd(a, b) = gcd(b, a % b) until b == 0; fold it over the whole list.
cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x): positive is a left turn, zero is collinear.
Keep the i-th item (1-based) with probability 1 / i: replace the pick when randrange(i) == 0.
Set k -= 1 so k counts from 0. At each slot, every choice starts a block of the same size, block (fact[n - 1 - slot] for permutations): take choice number k // block, then k %= block is the rank inside that block. For the rank of a given item, add (unused choices before its digit) * block at each slot, then add 1.
Pattern Architecture & Invariant Design Masterclass
Mathematical and geometric problems model continuous spaces using discrete structures. Invariants such as 4-pointer inward contraction for spiral matrices, greatest common divisor (GCD) reduction for normalized slope vectors, and 32-bit overflow guard checks ensure exact simulation without floating-point drift.
The invariants to say out loud:
- 4-Boundary Box: "After traversing the top row and right column, I must verify
top <= bottomandleft <= rightbefore reversing direction back across bottom and left." - Coprime Slopes: "Never compare floating-point slopes. Reduce displacement vectors by
gcd(dx, dy)and store coprime integer pairs."
🔬 Variable Anatomy: The 7 Variables Every Math & Geometry Solution Uses
To connect abstract geometric invariants with concrete Python syntax, here is the universal code anatomy showing what every variable does:
import mathfrom collections import defaultdict def mathGeometryMasterTemplate( matrix: list[list[int]], points: list[list[int]], n: int) -> tuple[list[int], int, int]: # ========================================================================= # 1. THE 4-BOUNDARY COORDINATE POINTERS # ========================================================================= top, bottom = 0, len(matrix) - 1 # ↕️ Vertical Boundary Bounds left, right = 0, len(matrix[0]) - 1 # ↔️ Horizontal Boundary Bounds spiral_res = [] # ========================================================================= # 2. 4-POINTER PERIMETER CONTRACTION (SPIRAL MATRIX) # ========================================================================= while top <= bottom and left <= right: # Top row: left -> right for c in range(left, right + 1): spiral_res.append(matrix[top][c]) top += 1 # Right col: top -> bottom for r in range(top, bottom + 1): spiral_res.append(matrix[r][right]) right -= 1 # Bottom row: right -> left (Guarded against single row) if top <= bottom: for c in range(right, left - 1, -1): spiral_res.append(matrix[bottom][c]) bottom -= 1 # Left col: bottom -> top (Guarded against single col) if left <= right: for r in range(bottom, top - 1, -1): spiral_res.append(matrix[r][left]) left += 1 # ========================================================================= # 3. GCD REDUCED FRACTION SLOPE HASHING # ========================================================================= slope_counts = defaultdict(int) if points: x1, y1 = points[0] for x2, y2 in points[1:]: dx = x2 - x1 dy = y2 - y1 if dx == 0 and dy == 0: continue g = math.gcd(dx, dy) reduced_slope = (dy // g, dx // g) # ⚡ Coprime integer slope key if reduced_slope[0] < 0 or (reduced_slope[0] == 0 and reduced_slope[1] < 0): reduced_slope = (-reduced_slope[0], -reduced_slope[1]) slope_counts[reduced_slope] += 1 # ========================================================================= # 4. 32-BIT OVERFLOW GUARD & DIGIT EXTRACTION # ========================================================================= INT_MAX = 2**31 - 1 INT_MIN = -2**31 rev = 0 temp_n = n sign = -1 if temp_n < 0 else 1 temp_n = abs(temp_n) while temp_n > 0: digit = temp_n % 10 temp_n //= 10 # ⚠️ CRITICAL: Check overflow BEFORE multiplying if rev > (INT_MAX - digit) // 10: rev = 0 break rev = rev * 10 + digit reversed_int = sign * rev return spiral_res, max(slope_counts.values(), default=0), reversed_int Variable Role Reference:
| Variable | Role in Code | Question It Answers | ❌ What Happens If Omitted or Mismanaged? |
|---|---|---|---|
top / bottom | Row boundary pointers | "What is the active vertical range of unvisited cells?" | ❌ Without contracting top += 1 / bottom -= 1, rows are traversed multiple times. |
left / right | Col boundary pointers | "What is the active horizontal range of unvisited cells?" | ❌ Omitting if top <= bottom before bottom sweep repeats middle rows on non-square matrices. |
dx / dy | Coordinate delta | "What is the displacement between two geometric points?" | ❌ Subtracting in inconsistent order (x1 - x2) vs (x2 - x1) inverts vector direction. |
g (gcd) | Greatest common divisor | "What factor reduces (dy, dx) to its unique coprime ratio?" | ❌ Using floating-point division dy / dx introduces precision errors where distinct slopes collide in hash keys. |
INT_MAX | 32-bit clamp threshold | "Will rev * 10 + digit exceed 32-bit signed integer capacity?" | ❌ Checking overflow after multiplying triggers runtime integer wrap/overflow in fixed-width languages. |
digit | x % 10 | "What is the current least significant base-10 digit?" | ❌ In Python, negative % 10 returns positive; in C++/Java it returns negative, breaking parity. |
exp | Binary power exponent | "Can we halve the exponent to reduce power calculation to ?" | ❌ Iterating times for times out on large exponents (). |
🧠 The Universal Contract of Discrete Geometry:
- Coprime Slope Contract: Never hash
float(dy) / float(dx)due to IEEE 754 precision drift. Always reduce byg = gcd(dx, dy)and hash the integer tuple(dy // g, dx // g). Handle vertical lines(1, 0)and horizontal lines(0, 1)consistently.- Boundary Guard Invariant: When contracting a 2D matrix box, the horizontal and vertical sweeps shrink the space. Always verify
top <= bottomandleft <= rightbefore initiating the return bottom and left sweeps.
🌟 The 3 Dimensions of Math & Geometry & The Precision-Boundary Invariant
Every Math & Geometry problem is governed by 3 Fundamental Dimensions:
┌─────────────────────────────────────────────────────────────────────────────┐│ THE 3 DIMENSIONS OF MATH & GEOMETRY │├───────────────────┬───────────────────────────────────┬─────────────────────┤│ Dimension │ Mechanism in Code │ Question It Answers │├───────────────────┼───────────────────────────────────┼─────────────────────┤│ 1. BOUNDARY │ Monotonic inward contraction: │ "How does perimeter ││ CONTRACTION │ `top, bottom, left, right` │ geometry shrink?" │├───────────────────┼───────────────────────────────────┼─────────────────────┤│ 2. ARITHMETIC │ Coprime reduction `(dy//g, dx//g)`│ "How do we preserve ││ PRECISION │ Pre-multiplication overflow guard │ exact equality?" │├───────────────────┼───────────────────────────────────┼─────────────────────┤│ 3. NUMBER THEORY │ Binary Exponentiation: O(log N) │ "How do we avoid ││ REDUCTION │ Sieve: O(N log log N) │ O(N^2) brute force ││ │ instead of trial division │ operations?" │└───────────────────┴───────────────────────────────────┴─────────────────────┘ How this plays out in actual problems:
🟢 Scenario A: Perimeter Box Contraction (Spiral Matrix LC 54)
- Traversing a matrix in spiral order naively requires an visited grid.
- The Rule: Maintain 4 boundary limits (
top = 0, bottom = R-1, left = 0, right = C-1). Traverse top edge (L➔R) and incrementtop += 1. Traverse right edge (T➔B) and decrementright -= 1. Guard inner reverse sweeps withif top <= bottomandif left <= right. - The Result: Complete traversal in time with extra space.
🔵 Scenario B: Coprime Slope Hashing (Max Points on a Line LC 149)
- Checking if 3 points are collinear by comparing
(y2 - y1) / (x2 - x1) == (y3 - y1) / (x3 - x1)fails on vertical lines (divide-by-zero) and floats. - The Rule: Compute
g = math.gcd(dx, dy). Normalize signs sody > 0(ordy == 0anddx > 0). Store coprime pairs(dy // g, dx // g)in a hash map. - The Result: Guaranteed zero precision drift across all lines.
🟣 Scenario C: Binary Exponentiation (Pow(x, n) LC 50)
- Computing with a loop takes multiplications, which times out for .
- The Rule: Halve the exponent each step: if is even, ; if is odd, .
- The Result: Collapses operations to about 31 squarings plus one multiplication per 1-bit of (at most about 62 multiplications, ).
💡 The Ultimate Takeaway Decision Rule:
When analyzing a math or coordinate problem, ask: "Is this perimeter navigation, slope collinearity, or large arithmetic reduction?"
- If Matrix spiral / rotation ➔ 4-Pointer Perimeter Contraction ().
- If Line collinearity / slopes ➔ Coprime GCD tuple hashing
(dy//g, dx//g).- If Large powers or primes ➔ Binary Exponentiation () or Sieve ().
- If 32-bit digit operations ➔ Pre-multiplication overflow guard
rev > (INT_MAX - d) // 10.
🎯 The 4 Essential Math & Geometry Paradigms & Archetypes
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐│ THE 4 CORE MATH & GEOMETRY ARCHETYPES │├────────────────────────┬──────────────────────┬───────────────────────────────┬──────────────────────────────────┤│ Archetype │ Coordinate Mechanism │ Exact Arithmetic Invariant │ Target Problems & Complexity │├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 1. Perimeter Contract. │ 4 shrinking bounds │ Guard reverse sweeps with │ Spiral Matrix (LC 54), Spiral ││ (4-Boundary Box) │ `top, bottom, L, R` │ `top <= bottom & left <= right│ Matrix II (LC 59) — O(R * C) │├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 2. Coprime GCD Slopes │ Coordinate deltas │ `g = gcd(dx, dy)` yields │ Max Points on a Line (LC 149) ││ (Collinear Points) │ `(x2 - x1, y2 - y1)` │ normalized `(dy//g, dx//g)` │ — O(N^2) │├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 3. Overflow Surgery │ Base-10 extraction │ Check `rev > (MAX - d) // 10` │ Reverse Integer (LC 7), String ││ (Digit Reversal) │ `d = x % 10; x //= 10` before multiplying by 10 │ to Integer atoi (LC 8) — O(log N)│├────────────────────────┼──────────────────────┼───────────────────────────────┼──────────────────────────────────┤│ 4. Logarithmic Powers │ Binary Exponentiation│ $x^n = (x^2)^{n/2}$; │ Pow(x, n) (LC 50) — O(log N); ││ (Sieve & Pow) │ Sieve crossing $p^2$ │ cross multiples in sieve │ Count Primes (LC 204) — ││ │ │ │ O(N log log N) │└────────────────────────┴──────────────────────┴───────────────────────────────┴──────────────────────────────────┘ 🌀 1. 4-Pointer Perimeter Contraction (LC 54 — Spiral Matrix)
- Mental Model: Peels rectangular perimeters layer-by-layer like an onion.
- Code: see the Spiral Matrix card.
📐 2. Number Theory & GCD Slope Hashing (LC 149)
- Mental Model: Points share a line if their normalized slope vectors from an anchor point are identical.
- Code: see the Canonical Key card.
🔢 3. 32-Bit Integer Overflow Surgery (LC 7)
- Mental Model: Reverse digits one by one while verifying that multiplying by 10 will not overflow 32-bit signed integer limits.
- Code: see the Digit Manipulation card.
⚡ 4. Binary Exponentiation (LC 50 — Pow(x, n))
- Mental Model: Repeated squaring halves the computation size at each step.
- Code: see the Fast Power card.
🌳 Evaluating Mathematical Invariants, Modular Arithmetic & Precision Guarantees
Maintaining exactness in mathematical calculations requires understanding computer arithmetic:
1. In-Place Matrix Rotation (Transpose + Reflect)
- To rotate an matrix 90 degrees clockwise in-place with extra space:
- Transpose: Swap
matrix[i][j]withmatrix[j][i]for all . - Reflect: Reverse each row
row.reverse().
- Transpose: Swap
2. Guarding Modulo of Negative Numbers
- In languages like C++ and Java,
-7 % 3yields-1rather than2. To guarantee a non-negative residue, use(x % m + m) % m.
💡 Staff SWE Architectural Invariant:
Avoid floating-point types for discrete geometry comparisons. Always reduce coordinate ratios to coprime integer pairs with GCD to eliminate precision drift across platforms.
Understanding Invariants: Perimeter Contraction & Coprime Slope Precision
Discrete geometry problems eliminate floating-point precision loss by reducing coordinate deltas to coprime integer ratios using GCD, and compress matrix state via 4-pointer perimeter box contraction.
Contracting 4-boundary boxes (top, bottom, left, right) guarantees every cell in a 2D matrix is visited exactly once in O(1) auxiliary space without a visited hash set.
Contracts boundary pointers after top and right sweeps; without guards, non-square matrices re-traverse cells.
Prevents IEEE 754 floating-point drift and avoids divide-by-zero crashes on vertical lines.
Common Questions
"In Spiral Matrix, why do the bottom-row and left-column sweeps need an extra if top <= bottom / if left <= right guard, when the top-row and right-column sweeps don't need one?"
Because by the time execution reaches the third and fourth sides of a lap, the first two sides may have already shrunk the remaining region down to a single row or column — and without the guard, that same row or column gets walked a second time, in the opposite direction.
Each full lap peels one layer off the "onion": top row left-to-right (
top += 1), right column top-to-bottom (right -= 1), bottom row right-to-left (bottom -= 1), left column bottom-to-top (left += 1). The first two sides are always safe because the loop conditionwhile top <= bottom and left <= rightalready guaranteed a valid region existed before the lap began. But the first two sides' own pointer updates (top += 1,right -= 1) happen during the lap — so by the time you reach the third side,topmay have already passedbottom(this was a single remaining row), or by the fourth sideleftmay have already passedright(a single remaining column).Without the guard, the bottom-row sweep would re-walk the exact same row the top-row sweep just finished, in reverse — double-counting cells that were already collected. The two guard checks exist purely to detect "this layer has already been fully collapsed to a line by the earlier sides of this same lap" and skip the redundant sweep. It's specific to the third and fourth sides because only they can observe a collapse the first two sides just caused.
Coach discussion points
- 1.
How do we cleanly represent vertical line slopes where
dx == 0when using the GCD approach? - 2.
Explain how coordinate matrix rotation
matrix[i][j]maps tomatrix[j][n - 1 - i]for a 90-degree clock-wise turn. - 3.
Why must
dividend == INT_MIN and divisor == -1be checked before any other arithmetic in Divide Two Integers, rather than after computingabs(dividend)? - 4.
Walk through why doubling
tempandmultipletogether in lock-step guarantees the bit-shift doubling algorithm removes at least half of the remaining dividend on every outer loop iteration, bounding total iterations toO(log N). - 5.
In Integer to Roman, why does putting the subtractive forms (900=CM, 400=CD, 90=XC, 40=XL, 9=IX, 4=IV) directly into the ordered value/symbol table eliminate the need for any per-digit conditional branching?
- 6.
What would break if the Integer to Roman value/symbol table were not kept in strictly decreasing order?
- 7.
For reverse integer, why does Python let you cheat the 32-bit limit naturally, and how do you enforce it algorithmically in Python?
- 8.
In Set Matrix Zeroes, why may row 0 and column 0 hold the flags at all, and why must
first_row_zeroandfirst_col_zerobe recorded before the first flag is written and used only after the second pass? - 9.
In Convex Polygon, why must a turn of
0be skipped before it is compared withprevor stored in it, and which two turns does the% nwrap-around add? - 10.
In Find Greatest Common Divisor of Array, why do
(a, b)and(b, a % b)share every common divisor, and why musta, b = b, a % bupdate both values in one statement? - 11.
In Linked List Random Node, why does keeping the i-th node with chance exactly 1/i leave every node with chance 1/n at the end, and why must
icount the current node before the draw? - 12.
In Permutation Sequence, why do the arrangements that share a first digit form one block of
(n - 1)!, and why mustk -= 1come beforek // block?
Complexity analysis
- Spiral Matrix: Time, Space.
- Max Points: Time, Space.
- Reverse Integer: Time, Space.
- Josephus: Time, Space.
- Pow(x, n): Time, Space.
- Count Primes: Time, Space.
- Integer to Roman: Time, Space (fixed 13-entry table).
- Divide Two Integers: Time, Space.
- Set Matrix Zeroes: Time, Space (flags in row 0 and column 0).
- Convex Polygon: Time, Space (one cross product per corner).
- Find Greatest Common Divisor of Array: Time, Space (Euclid's remainder steps on the two ends).
- Linked List Random Node: Time per
getRandom, Space (reservoir sampling, one walk per pick). - Permutation Sequence: Time, Space (Count and Skip:
Nslots, each with anO(N)pop).
Graphics engines frequently rotate textures or traverse grids spirally to encode image blocks. Collinear points detection is used in computer vision for Hough transforms to detect lines. Overflow checks are critical for low-level banking applications where integer maximums can wrap around to negative numbers, causing catastrophic faults.
💡 Don't worry if modular arithmetic or geometric slope hashing feels mathematical right now! Mathematical algorithms exploit algebraic identities and coordinate transformations to avoid exhaustive simulation. As you progress through the Archetype Variants (from Count Primes Sieve and Binary Exponentiation to Spiral Matrix and Max Points on a Line), each hands-on challenge will cement this intuition until guarding integer overflow and applying invariant formulas becomes second nature.
Traversing bottom row or left column when boundaries have crossed: After traversing right and down,
topandbottomorleftandrightmay have crossed (e.g. in single-row or single-column matrices). You must verifytop <= bottombefore traversing left, andleft <= rightbefore traversing up.Allocating visited matrix when 4 boundaries suffice: Maintaining an visited boolean matrix wastes auxiliary space. Four bounding pointers (
top, bottom, left, right) achieve auxiliary space.Off-by-one on boundary increments: Forgetting to advance boundaries (
top += 1,right -= 1,bottom -= 1,left += 1) causes the traversal to repeat the outer perimeter infinitely.
Staff+ Systems Architecture & Hardware Considerations
Triggers: "greatest common divisor (GCD)", "prime factorization / sieve", "matrix rotation / spiral traversal", "combinatorics and modular arithmetic". Algebraic equations, modular arithmetic, and discrete coordinate transformations.
Values up to or matrix dimensions up to . Brute-force simulation times out; mathematical closed forms or Euclidean algorithm drop complexity from to .
Integer overflow during multiplication prior to modulo; division in modular arithmetic requiring Fermat's Little Theorem / modular inverse; matrix boundary off-by-one errors during spiral traversals.