Math and number theory
The name of this chapter scares people, and yet it contains the least mathematics in the book. Interview maths problems rarely test mathematics. They test whether you can find the pattern, or the quantity that never changes — Nim is n % 4, the bulb problem is perfect squares, the majority element is cancellation. Only one engineering rule really matters: do not let a large number overflow. This chapter shows how to turn a problem that looks like a wall of formulas into "try two small cases and read off the pattern".
Two things: find the invariant, and do not overflow
One is how to attack the problem. The other is a rule the code cannot break.
First, how to attack these problems. They look frightening because they look like they need mathematics. But interview maths problems almost never use calculus or linear algebra. They ask something else: can you work through a few small cases by hand and notice a pattern, or an invariant — a quantity that stays the same through every step? Find it and the problem often collapses into one line. Miss it and you are stuck writing a brute-force simulation.
That is the pattern this chapter repeats. In the Nim game the invariant is the number of stones modulo 4. In the bulb problem it is whether the number of divisors is odd. For the majority element it is that the cancellation cannot finish. So the general routine is:
Build a table — compute a few small answers by hand
What is the answer for n = 1, 2, 3, 4, 5? For a game problem, work out who wins. For a counting problem, list the first few terms. It feels slow, but the pattern often only shows up at the fourth or fifth value.
Guess the pattern, or name the invariant
Look at the sequence you wrote down. Does it repeat with a period? Does it depend on parity? On perfect squares, or on multiples of 4? The quantity that no move can change is the key.
Prove it by induction — say why it always holds
A pattern is not a coincidence. Finish the argument with induction, or by showing that the opponent can always restore the invariant. In an interview, only a pattern you can justify is safe to write as code.
Now the rule the code cannot break: overflow and the modulus. Counting problems often have answers that are astronomically large (a number of combinations, a number of ways), so the statement asks for the result modulo 10⁹+7. That is not there to annoy you. It gives you a range where nothing overflows. But there is one rule: reduce as you compute, never only at the end. If you wait, the intermediate value has already overflowed into a wrong number, and a modulus applied afterwards cannot recover it. Reducing as you go is allowed because the modulus distributes over addition and multiplication:
(a + b) % m = ((a%m) + (b%m)) % m. Subtraction works the same way, but add m before the last reduction so the result is not negative.
(a × b) % m = ((a%m) × (b%m)) % m. This is the dangerous one: two values near 10⁹ multiply to about 10¹⁸, so the product must be held in a 64-bit long.
(a ÷ b) % m ≠ ((a%m) ÷ (b%m)) % m. To divide you need a modular inverse, which exists only when b and m share no factor above 1. Computing C(n, k) mod p is where you meet this. See Fermat's little theorem in §04.
% follows the sign of the divisor, so -7 % 3 is 2 and the result is never negative — the (x % m + m) % m correction is not needed. Both differences will surprise you if you switch to Java or C++ for an interview.Why does everyone use 10⁹+7?
It has to meet three conditions. First, it is large enough that two different answers rarely collide after the reduction. Second, it is prime, so every value that is not a multiple of it has a modular inverse and division becomes possible (see §04). Third, its square (10⁹+7)² ≈ 10¹⁸ still fits in a 64-bit long (limit ≈ 9.2×10¹⁸), so a product of two reduced values does not overflow. 10⁹+7 sits exactly where all three hold. Its relative 998244353 is also prime and is more common in competitive programming, because it works with the number theoretic transform (NTT).
Greatest common divisor: the Euclidean algorithm
gcd(a, b) = gcd(b, a % b) — one line of recursion, and here is why it is valid
The greatest common divisor (gcd) of two numbers is the largest number that divides both. The slowest method tries every value from min(a, b) downwards, which is O(min(a, b)). Euclid gave a much shorter route: gcd(a, b) = gcd(b, a % b). Repeat until the remainder is 0, and the divisor at that moment is the answer. This is the Euclidean algorithm.
Why is it valid? In one sentence: a and b have exactly the same common divisors as b and a % b. Suppose d divides both a and b. Because a % b = a − ⌊a/b⌋ × b, and every term on the right is a multiple of d, d also divides a % b. In the other direction, if d divides b and a % b, then a = ⌊a/b⌋ × b + (a % b) is a multiple of d as well. The two sets of common divisors are identical, so their largest members are equal — which is why replacing the pair with a smaller pair is safe.
How many steps? The remainder shrinks fast: after every two steps the larger value is at least halved. So the algorithm finishes in O(log min(a, b)) steps, and the gcd of two 64-bit numbers takes only a few dozen divisions.
gcd(48, 36): 48 % 36 = 12, so it becomes gcd(36, 12)
A large problem becomes a smaller one, and the set of common divisors does not change.
gcd(36, 12): 36 % 12 = 0, so it becomes gcd(12, 0)
A remainder of 0 means 12 divides 36, so the recursion is about to stop.
gcd(12, 0) = 12 — the remainder is 0, so the divisor is the answer
The gcd of any number and 0 is that number itself. The answer is 12.
a, b = b, a % b performs the whole step in one line, because the right side is evaluated before either assignment. From Python 3.9 you can call math.gcd and math.lcm directly, and both accept more than two arguments.Possibly the oldest algorithm still in daily use
The Euclidean algorithm appears in Book VII of Euclid's Elements, written around 300 BC. Twenty-three centuries later, the gcd you write today is the same procedure. It is also part of the base of modern cryptography: RSA key generation uses the extended Euclidean algorithm to find modular inverses. The extended version returns x and y with ax + by = gcd(a, b), and when gcd(a, b) = 1 that x is the modular inverse of a modulo b — which is how you invert a value when the modulus is not prime.
Worked example A · The sieve: cross out instead of testing
MEDIUMLC 204 Count Primes — turning the question round takes O(n√n) down to O(n log log n)
The problem: count the primes below n. Brute force: for each x, try dividing by every value from 2 to √x. One number costs O(√x), so the total is about O(n√n), which is already slow at n = 10⁶.
Why is √x enough? If x = a × b and both factors were larger than √x, then a × b would be larger than x, which is impossible. So at least one of the two factors is at most √x. Any divisor above √x always comes with a partner below it, and testing up to √x finds that partner.
Turning it round: deciding whether a single x is prime is slow, but saying which numbers are composite is easy — a composite is a multiple of some prime. So stop testing numbers one by one. Instead, take each prime and cross out all of its multiples, and whatever is left is prime. This is the sieve of Eratosthenes. Watch it find the primes below 30:
The animation contains the two questions this problem is really about. First, why does the marking start at i²? When the sieve reaches the prime i, every multiple of i below i² has a prime factor smaller than i, and that smaller prime already crossed it out. Starting at i² skips all the repeated work. Second, why does the outer loop only need to reach √n? Every composite x ≤ n has a factor no larger than √x ≤ √n, so once all the primes up to √n have swept the board, everything still standing is prime.
is_prime[i*i : n : i] = [False] * k crosses out a whole run of multiples in one operation. It is much faster than a Python-level for loop, because the slice is implemented in C. sum over a list of booleans then counts the survivors.Complexity and follow-up questions
Time O(n log log n), space O(n). Three common follow-ups. (1) Where does log log n come from? Summing n/p over the primes p below n gives about n·ln ln n. A frequent wrong answer here is O(n log n); the real bound is smaller than that. (2) Can it be O(n)? Yes, with the linear sieve (Euler's sieve): keep a list of primes and an array of smallest prime factors, so every composite is crossed out exactly once, by its smallest prime factor. The key line is to break out of the inner loop as soon as i % prime == 0. (3) What if you only need to test one large number? Do not sieve — use the Miller-Rabin primality test.
In production: every HTTPS handshake
Public-key systems such as RSA and Diffie-Hellman rest on one asymmetry: finding two large primes is easy, and factoring their product back is very hard. When a key is generated, the machine keeps picking a large random number and running a primality test. At that size the test is the probabilistic Miller-Rabin, not a sieve — you cannot sieve numbers around 2²⁰⁴⁸. Trial division by small primes is still used as a cheap pre-filter, and that is the sieve's idea of removing multiples of small primes, applied to one number at a time.
Fast power (review): from b multiplications down to log b
Review · divide and conquerThe divide and conquer tool from chapter 02, this time with the modulus
Computing a^b by multiplying b times is O(b), which is hopeless at b = 10⁹. Fast exponentiation (also called binary exponentiation), covered in the divide and conquer chapter, brings it down to O(log b). The identity is a^b = (a²)^(b/2): square the base and halve the exponent. Equivalently, read the binary digits of b, and for every digit that is 1, multiply the matching a^(2ᵏ) into the result. b has only log b digits, so there are only log b squarings. Here we review the companion it always has in number theory: take the modulus after every multiplication.
Why discuss the modulus here? Because §01 left one gap: you cannot divide under a modulus, and fast power is the tool that closes it. Fermat's little theorem says that when m is prime and a is not a multiple of m, a^(m−1) ≡ 1 (mod m). Multiply both sides by the inverse of a and you get that the modular inverse of a is a^(m−2) mod m. So to compute (x ÷ a) % m, compute (x × a^(m−2)) % m instead — one call to fast power. Both conditions matter. If m is not prime, this formula is wrong, and you need the extended Euclidean algorithm from §02 instead.
pow(a, b, mod) is the built-in fast power, implemented in C and much faster than a hand-written loop. pow(a, -1, mod) (Python 3.8 and later) returns the modular inverse directly, and it works for any modulus that is coprime to a, not only for a prime one. Write the loop by hand in an interview; call the built-in everywhere else.The same skeleton computes matrices, not only numbers
Fast power is really this: for any operation that is associative, split the count in binary and turn n operations into log n. Replace multiplication of numbers with multiplication of matrices and you get matrix fast power, which gives an O(log n) solution for Fibonacci and stair climbing (chapter 07 on DP sets this up) and is fast enough for n = 10¹⁸. Any linear recurrence can be accelerated the same way.
Worked example B · Boyer-Moore voting: let the majority appear on its own
EASYLC 169 Majority Element — O(1) space, from a single cancellation argument
The problem: one element appears more than n/2 times (the statement guarantees it exists); find it. Brute force: count with a hash map, O(n) time but O(n) space; or sort and take the middle element, O(n log n). Both are accepted. Neither is the best answer.
Can it be O(n) time and O(1) space? Yes, with one invariant. Keep a single candidate and a single count. For each value in turn: if it equals the candidate, add 1; if it differs, subtract 1; and when the count reaches 0, the next value becomes the candidate. This is Boyer-Moore voting. Step through it:
Why is the value left at the end always the majority? Read every −1 step as discarding a pair: one copy of the current candidate and one copy of a different value are thrown away together. So each −1 removes two values that are not equal. Now suppose one value appears more than n/2 times. All the other values together number fewer than n/2, so they run out before its copies do. It cannot be paired away completely, so it is the value that survives. That is the invariant that always holds.
collections.Counter(nums).most_common(1) also gives the answer in one line, but it uses O(n) space. The interview wants the O(1) space version, and it wants you to state the invariant out loud.Complexity and follow-up questions
Time O(n), space O(1). The classic extensions. (1) Find all elements appearing more than n/3 times (LC 229). At most two elements can, so keep two candidates and two counts, apply the same rules, and verify both at the end. (2) More than n/k? Keep k−1 candidates; this is the Misra-Gries algorithm. (3) Why verify instead of returning directly? When existence is not guaranteed, the scan still returns some surviving candidate, and that candidate need not appear more than n/2 times.
In production: heavy hitters in a data stream
When you can read the data only once and cannot hold all of it in memory — finding the IP address sending the most packets, or the most frequent error code in a log — the generalisation of Boyer-Moore voting is the standard answer. Misra-Gries and Space-Saving keep a fixed number of counters and report the approximately most frequent elements. This is the classic solution to the heavy hitters problem. The cancellation idea, in O(1) space, is what makes it possible.
Next permutation: carrying in lexicographic order
MEDIUMLC 31 — no arithmetic at all, only a pattern about positions
The problem: rearrange the array into the next permutation in lexicographic order, that is, the smallest arrangement that is still larger than the current one. If it is already the largest, return the smallest. Brute force: generate all n! permutations, sort them, and look up the next one — already impossible at n = 10. The solution: think of it as adding one to a number and carrying. The pattern has four steps.
The intuition: if the suffix of a permutation is decreasing, that suffix is already its own largest arrangement, so nothing inside it can grow and the increase has to come from the position in front of it. So scan from the right for the first position that breaks the decreasing run, replace its value with the smallest value to its right that is still larger, and then make the right part as small as possible by putting it in increasing order. Frame by frame:
nums[i+1:] = reversed(nums[i+1:]) reverses the right part in one line. Slice assignment modifies the original list, which matches the requirement to change the array in place and return nothing.In an interview, state the pattern before you write code
For a pattern-finding problem, describe the rule first: "The next permutation: from the right, find the first position that can grow, replace it with the smallest larger value from its right, then make the right part as small as possible." Once the rule is clear, the interviewer knows you understand it, and the code is only those sentences turned into loops. If you cannot state the rule, the code will not come out cleanly either.
Worked example C · Game theory: find the losing position
EASYLC 292 Nim Game — the general method is to build a table and read off an invariant
The problem: n stones on the table. You and your opponent take turns removing 1 to 3 stones, and the player who takes the last stone wins. Both play perfectly and you move first. Can you win? Brute force: a game search (minimax) over every possible move; the state space is O(n) and it times out. The solution: the routine from §01 — build a table, guess the pattern, prove it.
Play the small cases. n = 1, 2, or 3: take them all, so the first player wins. n = 4: whatever you take (1 to 3), you leave 1 to 3 stones for your opponent, who takes them all, so the first player loses. n = 5, 6, or 7: take 1, 2, or 3 to leave exactly 4 — the losing position — for your opponent, so the first player wins. n = 8: every move leaves 5, 6, or 7, all winning positions for your opponent, so the first player loses again. The pattern appears: the first player loses when n is a multiple of 4 and wins otherwise. The invariant is n % 4. Try it yourself — can you escape from a losing position?
Why does the invariant hold (proof by induction)? Call a position losing when the number of stones left is a multiple of 4 and it is your turn. (1) Base case: 0 stones and your turn means the previous player took the last stone and you have no move, so you lose — and 0 is a multiple of 4, which fits. (2) Induction: if you face a multiple of 4 and take k stones (1 to 3), your opponent takes 4−k and the pile is a multiple of 4 again, handed back to you. Repeating this leaves you 0. In the other direction, if n is not a multiple of 4, take n % 4 on the first move and your opponent faces a losing position. So the answer is one line:
The same "find the invariant" move clears a whole group of game and pattern problems. Two relatives from this chapter's problem set:
Players take turns replacing n by n − x, where x is a proper divisor (0 < x < n and n % x == 0). A player with no legal move loses. Building the table shows: the first player wins for even n and loses for odd n. An even n lets you subtract 1 and hand over an odd number, while every divisor of an odd number is odd, so subtracting always produces an even number. The invariant is the parity of n.
Bulb i is toggled once for each divisor of i. Divisors come in pairs, d and i/d, so the count is even unless d = i/d, which happens only for a perfect square. Only those bulbs are left on, and the answer is ⌊√n⌋. The invariant is the parity of the number of divisors.
The real Nim, and a theorem from 1901
The multi-pile version of Nim was fully solved by the Harvard mathematician Charles Bouton in 1901: take the XOR of the pile sizes, and the first player wins exactly when the result is not 0. The Sprague-Grundy theorem later extended this to every impartial combinatorial game — each position gets a Grundy number, and the outcome of the whole game is the XOR of the Grundy numbers of its independent parts. A large part of combinatorial game theory starts from this small game with stones.
Happy number: restating a number problem as cycle detection
EASYLC 202 — the useful step is seeing that this is a chain that can loop
The problem: repeatedly replace a number by the sum of the squares of its digits. If it reaches 1, the number is called happy. For example 19: 1²+9² = 82, then 8²+2² = 68, then 6²+8² = 100, then 1²+0²+0² = 1. Happy.
The difficulty: a number that is not happy repeats forever and never reaches 1. Take 2:
The observation: treat "the next number" as a pointer — every number points at the sum of the squares of its digits. The whole process is then a linked list. For a happy number the chain ends at 1, which you can read as 1 pointing to itself. For an unhappy number the chain comes back to a value it has already visited, forming a cycle. So "does this chain reach 1" is the same question as "does this linked list contain a cycle" — which is the fast and slow pointer method (Floyd's cycle detection) from DataData · 03. Move the slow pointer one step and the fast pointer two steps: if they meet there is a cycle and the number is not happy; if the fast pointer reaches 1 first, the number is happy.
Why must it either reach 1 or repeat, instead of growing forever? Because the sum of squared digits cannot run away. The largest three-digit number is 999, and its sum of squares is 3 × 9² = 243, which is smaller than 999. The more digits a number has, the further the sum of squares falls behind it. So the value is quickly pushed into the finite range [1, 243]. A walk that keeps moving inside a finite set of states must, by the pigeonhole principle, either hit 1 or repeat a value — and a repeat is a cycle. There is always an outcome.
str(x) is the shortest way to split the digits. For speed, use a divmod(x, 10) loop instead. The hash-set version is seen = set() then while n != 1 and n not in seen: ....The same trick: “can this loop” is a whole family of problems in disguise
Whenever a process has the property that the current state alone decides the next state, it is a hidden linked list, and the question "does it enter a loop" can be answered with two pointers at different speeds. LC 202 Happy Number, LC 287 Find the Duplicate Number (where nums[i] is read as the next pointer), and detecting the period of an iterated function are all the same Floyd cycle detection. Recognising the disguise is worth more than memorising the algorithm.
Problem set: 13 problems on maths and number theory
Core + optionalGrouped by pattern finding, modular arithmetic, and game theory, easy to hard. Think for 30 seconds before opening the hint.
Quiz
✎ QuizGet all 8 right to mark this chapter complete.
A long chain of multiplications must be reduced modulo 10⁹+7. Which approach actually avoids overflow?
Which of these modular identities is false?
Use the Euclidean algorithm to compute gcd(48, 36).
In the sieve of Eratosthenes, why does the marking of multiples of i start at i×i instead of 2×i?
Boyer-Moore voting finds the element that appears more than n/2 times. What does it require?
Nim: n stones on the table, players alternately take 1 to 3 stones, and the player who takes the last stone wins. With n = 12, what happens to the first player?
Which of these problems are solved by finding an invariant or a pattern, rather than by applying a complicated formula? (Select all that apply.)
Happy number: repeatedly replace a number by the sum of the squares of its digits; the number is happy if this reaches 1. How do you decide reliably that it will never reach 1?
- The core of this chapter: interview maths problems test whether you can find an invariant or a pattern. The routine is build a table (compute a few small cases) → guess the pattern or invariant → prove it by induction.
- Modulus rules: addition, subtraction, and multiplication can all be reduced as you compute, because the modulus distributes over them. Division cannot — it needs a modular inverse. The rule is reduce at every step, never only at the end, because by then the intermediate value has already overflowed.
- Why 10⁹+7: large enough, prime (so inverses exist), and its square still fits in a 64-bit long. Java overflows silently, JavaScript loses precision above 2⁵³ (use BigInt), Python has unbounded integers but still reduces to keep values small. Negative
%also differs: Java and JavaScript follow the sign of the dividend, Python follows the divisor. - gcd(a, b) = gcd(b, a % b), because both pairs have exactly the same common divisors, and it finishes in O(log min(a, b)) steps. lcm(a, b) = a / gcd × b, dividing first to avoid overflow. The sieve marks multiples from i² and only needs i up to √n, giving O(n log log n). Fast power is O(log b) and, with Fermat's little theorem, gives modular inverses when the modulus is prime.
- Boyer-Moore voting: the cancellation argument finds the majority element in O(1) space — provided a majority element really exists. If that is not guaranteed, verify the candidate with a second pass.
- Game problems: find the losing position. Nim uses n % 4, the divisor game uses parity, the bulb problem uses perfect squares. Once you know who faces the losing position, the problem collapses into one line.
- Two useful restatements: next permutation is carrying in lexicographic order (find the break → swap in the smallest larger value → reverse the right part); happy number is cycle detection in a linked list (the next function is the pointer, and two pointers at different speeds find the cycle).