組合數學與數學模式
範圍 — 面試會考到的計數與數論:模運算、GCD/LCM、篩法、nCr、蓄水池抽樣,以及基本幾何。 另見:math.md — 一般的數值操作;dp.md — 用遞推而非公式來計數;bit_manipulation.md — 位元層級的數論。
LeetCode 題目清單
總覽
Google 面試很常考數學/組合推理,比其他 FAANG 都多。這份文件涵蓋在程式面試中常見的數論、計數、幾何與機率模式。
關鍵性質
- 什麼時候用:題目牽涉到排列組合計數、模運算、GCD/LCM、質數,或幾何計算
- Google 想看的訊號:你能不能推出公式,而不是硬爆搜?
模式 1:模運算
基本規則
(a + b) % m = ((a % m) + (b % m)) % m
(a * b) % m = ((a % m) * (b % m)) % m
(a - b) % m = ((a % m) - (b % m) + m) % m ← add m to avoid negative
Division: (a / b) % m = (a * b^(-1)) % m where b^(-1) = modular inverse
模冪運算(快速冪) — LC 50
// Time: O(log exp), Space: O(1)
long modPow(long base, long exp, long mod) {
long result = 1;
base %= mod;
while (exp > 0) {
if ((exp & 1) == 1) result = result * base % mod;
exp >>= 1;
base = base * base % mod;
}
return result;
}
# Python has built-in: pow(base, exp, mod)
模反元素(模數為質數時)
b^(-1) mod p = b^(p-2) mod p (Fermat's little theorem)
經典 LC: LC 1808(Maximize Number of Nice Divisors)、LC 372(Super Pow)
變形:前綴和模 K — LC 523
轉折:把 (a - b) % m 反過來用 — 兩個餘數相同的前綴和,中間夾出來的子陣列就能被 k 整除。
// java
// LC 523 - Continuous Subarray Sum
// IDEA: sum(i..j) % k == 0 <=> prefix[j] % k == prefix[i-1] % k
// so store the FIRST index of each remainder and check the gap is >= 2.
// time = O(N), space = O(min(N, K))
public boolean checkSubarraySum(int[] nums, int k) {
Map<Integer, Integer> firstIdx = new HashMap<>();
firstIdx.put(0, -1); // empty prefix
int run = 0;
for (int i = 0; i < nums.length; i++) {
run = (run + nums[i]) % k;
Integer j = firstIdx.get(run);
if (j != null) {
if (i - j >= 2) return true; // need length >= 2
} else {
firstIdx.put(run, i); // keep the EARLIEST index only
}
}
return false;
}
# python
# LC 523 - Continuous Subarray Sum
# IDEA: sum(i..j) % k == 0 <=> prefix[j] % k == prefix[i-1] % k
# so store the FIRST index of each remainder and check the gap is >= 2.
# time = O(N), space = O(min(N, K))
def checkSubarraySum(nums, k):
first = {0: -1}
run = 0
for i, v in enumerate(nums):
run = (run + v) % k
if run in first:
if i - first[run] >= 2:
return True
else:
first[run] = i
return False
鴿籠原理的推論:餘數只有 k 種,所以任意 k+1 個前綴必定重複一次 — 只要 N >= k,就一定存在某個非空子陣列可被 k 整除。但重複可能出現在相鄰的兩個前綴,也就是長度 1,而 LC 523 不接受([1, 0]、k = 2 滿足 N >= k,卻沒有合法答案)。要保證長度 >= 2,需要 N >= 2k — 只對偶數索引的前綴 p0, p2, p4, ... 套鴿籠原理,這樣任何重複本身就已經相距 >= 2。(2k 是緊的 — N = 2k - 1 不夠:[0, 1, 0]、k = 2。)
變形:鴿籠原理 → 有限狀態空間必定進入循環 — LC 957
轉折:把同一套鴿籠論證從餘數換成整個狀態 — 只要狀態空間有限、轉移函數是確定性的,這條路徑就一定會循環,於是可以把巨大的 n 對週期取模。
做法(任何「把 f() 套用 n 次,n 大到 1e9」的題目都能重用):
1. hash each visited state -> the day it first appeared
2. on a repeat at day `d` with first sighting `f`: period = d - f
3. remaining = (n - d) % period ← burn off whole cycles
4. step `remaining` more times and return
LC 957 為什麼會終止:第 1 天之後兩端永遠是 0,只有中間 6 格會變 → 可達狀態最多 2^6 = 64 個。
// java
// LC 957 - Prison Cells After N Days
// IDEA: PIGEONHOLE — <=64 reachable states, so the day-sequence is eventually
// periodic. Find the period by hashing states, then n %= period.
// time = O(S * 8) with S = distinct states (<= 64), space = O(S)
public int[] prisonAfterNDays(int[] cells, int n) {
Map<String, Integer> seen = new HashMap<>(); // state -> day first seen
int day = 0;
while (day < n) {
String key = Arrays.toString(cells);
Integer first = seen.get(key);
if (first != null) { // cycle detected
int period = day - first;
int remain = (n - day) % period;
for (int i = 0; i < remain; i++) cells = nextDay(cells);
return cells;
}
seen.put(key, day);
cells = nextDay(cells);
day++;
}
return cells;
}
private int[] nextDay(int[] c) {
int[] out = new int[8]; // ends stay 0 by construction
for (int i = 1; i < 7; i++) out[i] = (c[i - 1] == c[i + 1]) ? 1 : 0;
return out;
}
# python
# LC 957 - Prison Cells After N Days
# time = O(S * 8), space = O(S)
def prisonAfterNDays(cells, n):
seen, day = {}, 0
while day < n:
key = tuple(cells)
if key in seen: # cycle detected
period = day - seen[key]
for _ in range((n - day) % period):
cells = next_day(cells)
return cells
seen[key] = day
cells = next_day(cells)
day += 1
return cells
def next_day(c):
return [0] + [1 if c[i - 1] == c[i + 1] else 0 for i in range(1, 7)] + [0]
同一套「把狀態雜湊起來 → 偵測週期 → 把循環數取模掉」的骨架,可以解掉任何 狀態有界、但步數大到荒謬的模擬題。如果你只需要偵測循環(不需要知道起始偏移), Floyd 的龜兔賽跑用 O(1) 空間就能做到。
模式 2:GCD/LCM
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
long lcm(long a, long b) { return a / gcd(a, b) * b; } // divide first to avoid overflow
from math import gcd
lcm = a * b // gcd(a, b)
# Python 3.9+: math.lcm(a, b)
經典 LC:
- LC 1071 GCD of Strings — O(N+M)
- LC 878 Nth Magical Number — LCM + 二分搜尋
- LC 2344 Minimum Deletions to Make Array Divisible — 目標陣列的 GCD
模式 3:質數與篩法
埃拉托斯特尼篩法 — LC 204
// Time: O(N log log N), Space: O(N)
boolean[] sieve(int n) {
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true);
isPrime[0] = isPrime[1] = false;
for (int i = 2; i * i <= n; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
return isPrime;
}
質因數分解
// Time: O(√N)
List<int[]> primeFactors(int n) {
List<int[]> factors = new ArrayList<>();
for (int i = 2; i * i <= n; i++) {
int count = 0;
while (n % i == 0) { n /= i; count++; }
if (count > 0) factors.add(new int[]{i, count});
}
if (n > 1) factors.add(new int[]{n, 1});
return factors;
}
經典 LC: LC 204(Count Primes)、LC 952(Largest Component by Common Factor)
相異質因數/所有因數的變化題、對整個陣列做分解的 SPF 篩法,以及 LC 4032,都放在
math.md的 1-1-2’) get factors of x。這裡只保留下面計數公式會用到的(質數, 指數)形式。
Legendre 公式 — n! 中某個質數的指數 — LC 172 Priority 4 of 5 — High value — a gap here costs you rounds
核心想法:不必真的算出 n!,就能直接讀出質數 p 在 n! 中的次方。
推導:在 1..n 之中,剛好有 ⌊n/p⌋ 個數至少貢獻一個因數 p,其中 ⌊n/p²⌋ 個再貢獻第二個,依此類推。把每一層加起來:
e_p(n!) = ⌊n/p⌋ + ⌊n/p²⌋ + ⌊n/p³⌋ + ... (stops once p^k > n)
Example e_5(30!) = ⌊30/5⌋ + ⌊30/25⌋ = 6 + 1 = 7
Trailing zeros of n! = min(e_2, e_5) = e_5
→ 2s are strictly more plentiful than 5s, so the 5s are always the bottleneck.
// java
// LC 172 - Factorial Trailing Zeroes
// IDEA: LEGENDRE'S FORMULA — e_p(n!) = sum_{k>=1} floor(n / p^k).
// A trailing zero needs one 2 and one 5; 5s are scarcer, so count them.
// time = O(log_p N), space = O(1)
public int trailingZeroes(int n) {
return legendre(n, 5);
}
// reusable helper: exponent of prime p in n!
int legendre(int n, int p) {
int e = 0;
for (long pk = p; pk <= n; pk *= p) e += (int) (n / pk); // long pk: p^k can overflow int
return e;
}
# python
# LC 172 - Factorial Trailing Zeroes
# time = O(log_p N), space = O(1)
def legendre(n, p):
e, pk = 0, p
while pk <= n:
e += n // pk
pk *= p
return e
def trailingZeroes(n):
return legendre(n, 5)
它在 LC 172 之外的價值:legendre() 是處理任何「階乘/二項式的整除性」問題的工具 — 例如 p 能整除 C(n, r) 幾次,就是 e_p(n!) - e_p(r!) - e_p((n-r)!);而能整除 n! 的最大 p^k 就是 p^legendre(n,p)。
模式 4:組合與計數
用 Pascal 三角求 nCr
// Time: O(N·K), Space: O(N·K)
long[][] pascal(int n) {
long[][] C = new long[n + 1][n + 1];
for (int i = 0; i <= n; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++) {
C[i][j] = C[i-1][j-1] + C[i-1][j]; // add % MOD if needed
}
}
return C;
}
用模反元素求 nCr(N 很大時)
MOD = 10**9 + 7
def nCr(n, r):
if r > n: return 0
num = den = 1
for i in range(r):
num = num * (n - i) % MOD
den = den * (i + 1) % MOD
return num * pow(den, MOD - 2, MOD) % MOD
Catalan 數 — LC 96
C(n) = C(2n, n) / (n+1) = (2n)! / ((n+1)! * n!)
C(0)=1, C(1)=1, C(2)=2, C(3)=5, C(4)=14, C(5)=42
Applications:
- Number of valid parentheses sequences of length 2n
- Number of unique BSTs with n nodes
- Number of ways to triangulate a polygon with n+2 sides
經典 LC: LC 96(Unique BSTs)、LC 22(Generate Parentheses 的計數版)
格子圖上的路徑 — LC 62 Priority 5 of 5 — Must know — expect it in almost every loop
核心想法:格子圖上的單調路徑,其實就是一個由 D 與 R 組成的字串。選路徑等於選哪些步是 D。這就把 O(m·n) 的 DP 變成一個二項式係數。
m x n grid, moves right/down only:
total steps = (m-1) downs + (n-1) rights = m+n-2
a path <=> which (m-1) of those m+n-2 slots are "down"
paths = C(m+n-2, m-1) = C(m+n-2, n-1)
3 x 7 grid -> C(8, 2) = 28
乘法形式(千萬別真的去算階乘,long 很快就爆掉):
C(N, r) = Π_{i=1..r} (N-r+i) / i,而第 i 步之後的部分乘積剛好是 C(N-r+i, i),是整數 — 所以每一步用整數除法都不會失準。
// java
// LC 62 - Unique Paths
// IDEA: COMBINATORICS — every path is a permutation of (m-1) D's and (n-1) R's,
// so answer = C(m+n-2, m-1). Multiply/divide alternately to stay in range.
// time = O(min(m,n)), space = O(1)
public int uniquePaths(int m, int n) {
int total = m + n - 2;
int r = Math.min(m - 1, n - 1); // pick the smaller half -> fewer iterations
long res = 1;
for (int i = 1; i <= r; i++) {
res = res * (total - r + i) / i; // exact: res == C(total-r+i, i) each step
}
return (int) res;
}
# python
# LC 62 - Unique Paths
# time = O(min(m,n)), space = O(1)
from math import comb
def uniquePaths(m, n):
return comb(m + n - 2, m - 1)
# manual version (no math.comb) — same alternate multiply/divide trick
def uniquePathsManual(m, n):
total, r = m + n - 2, min(m - 1, n - 1)
res = 1
for i in range(1, r + 1):
res = res * (total - r + i) // i
return res
面試時值得順口說出的推廣:
| 額外限制 | 答案 |
|---|---|
單純 m x n,只能往右/往下 |
C(m+n-2, m-1) |
必須經過某個檢查點 p |
paths(start→p) * paths(p→end) |
| 有障礙物/被封鎖的格子 | 封閉解失效 → 退回 O(m·n) DP |
| 路徑必須維持在對角線下方(含對角線) | Catalan C(n)(見上)— 也就是投票問題 |
階乘進位制(排列的編號與還原) — LC 60 Priority 4 of 5 — High value — a gap here costs you rounds
核心想法:n! 種排列按字典序編號為 0 .. n!-1。固定第一個元素,就框住連續的 (n-1)! 個排列,所以把編號寫成階乘進位制的每一位數,就直接對應每一步的選擇 — 不用列舉,也不用回溯。
rank = d_{n-1}*(n-1)! + d_{n-2}*(n-2)! + ... + d_1*1! + d_0*0!, 0 <= d_i <= i
n=4, k=9 -> rank 8 (0-indexed), pool [1,2,3,4]
8 / 3! = 1 rem 2 -> take pool[1] = 2 pool -> [1,3,4]
2 / 2! = 1 rem 0 -> take pool[1] = 3 pool -> [1,4]
0 / 1! = 0 rem 0 -> take pool[0] = 1 pool -> [4]
0 / 0! = 0 -> take pool[0] = 4
=> "2314"
// java
// LC 60 - Permutation Sequence
// IDEA: UNRANKING via the factorial number system. Fixing the i-th element
// skips blocks of (i-1)! permutations => digit = k / (i-1)!, k %= (i-1)!.
// time = O(N^2) (List.remove is O(N)), space = O(N)
public String getPermutation(int n, int k) {
int[] fact = new int[n + 1];
fact[0] = 1;
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i; // n <= 9, fits int
List<Integer> pool = new ArrayList<>();
for (int i = 1; i <= n; i++) pool.add(i);
k--; // convert to 0-indexed rank
StringBuilder sb = new StringBuilder();
for (int i = n; i >= 1; i--) {
int idx = k / fact[i - 1];
k %= fact[i - 1];
sb.append(pool.remove(idx)); // remove keeps `pool` sorted
}
return sb.toString();
}
# python
# LC 60 - Permutation Sequence
# time = O(N^2), space = O(N)
from math import factorial
def getPermutation(n, k):
pool = list(range(1, n + 1))
k -= 1 # 0-indexed rank
out = []
for i in range(n, 0, -1):
idx, k = divmod(k, factorial(i - 1))
out.append(pool.pop(idx))
return "".join(map(str, out))
反方向(求編號) — 常見的追問是「給你一個排列,它的索引是多少?」。第 d_i 位 = 還沒用過、且比 perm[i] 小的值有幾個:
// java
// Inverse of LC 60: 0-indexed lexicographic rank of a permutation
// time = O(N^2) brute (use a BIT for O(N log N)), space = O(1)
long rankOf(int[] perm) {
int n = perm.length;
long rank = 0, f = 1;
for (int i = n - 1; i >= 0; i--) {
int smaller = 0;
for (int j = i + 1; j < n; j++) if (perm[j] < perm[i]) smaller++;
rank += smaller * f; // f == (n-1-i)! at this point
f *= (n - i);
}
return rank;
}
# python
# Inverse of LC 60: 0-indexed lexicographic rank of a permutation
# time = O(N^2), space = O(1)
def rank_of(perm):
n = len(perm)
rank, f = 0, 1
for i in range(n - 1, -1, -1):
smaller = sum(1 for j in range(i + 1, n) if perm[j] < perm[i])
rank += smaller * f
f *= (n - i)
return rank
rankOf其實是在數右邊的逆序對 —n很大時把內層迴圈換成樹狀陣列(Fenwick),可做到 O(N log N)。
依貢獻度計數 — LC 477Priority 5 of 5 — Must know — expect it in almost every loop
核心想法:當一個總和是跑遍所有配對/子陣列/子集合時,不要去走訪這些結構 — 把迴圈翻過來問:「每個最小單位分別出現在幾個結構裡?」 這會把 O(N²)(甚至更糟)的總和,變成 O(N) × (每個單位一次便宜的計數)。
sum over structures == sum over atoms of (value) * (# structures containing it)
Two counts worth memorising:
- pairs across a binary split : ones * (n - ones)
- subarrays containing index i: (i + 1) * (n - i)
以 LC 477 來說,各個位元互相獨立(Hamming 距離是逐位元相加),所以固定一個位元位置:在該位不同的配對數,剛好是 ones × (n - ones)。
// java
// LC 477 - Total Hamming Distance
// IDEA: COUNT BY CONTRIBUTION. Skip the C(n,2) pairs; ask what each BIT adds.
// At a bit with `ones` set and `n-ones` clear, exactly ones*(n-ones) pairs differ.
// time = O(32*N), space = O(1)
public int totalHammingDistance(int[] nums) {
int n = nums.length, total = 0;
for (int b = 0; b < 32; b++) {
int ones = 0;
for (int x : nums) ones += (x >> b) & 1;
total += ones * (n - ones); // pairs differing at bit b
}
return total;
}
# python
# LC 477 - Total Hamming Distance
# time = O(32*N), space = O(1)
def totalHammingDistance(nums):
n, total = len(nums), 0
for b in range(32):
ones = sum((x >> b) & 1 for x in nums)
total += ones * (n - ones)
return total
怎麼認出來:題目說「對所有配對/所有子陣列求和」,而 N 大到直接雙層迴圈會 TLE。能把總和拆成每個單位的貢獻,靠的就是獨立性(位元、位數、索引)。
模式 5:蓄水池抽樣與隨機
蓄水池抽樣(K=1) — LC 382
// Select 1 random element from stream of unknown length
// Each element has 1/n probability of being chosen
// Time: O(N), Space: O(1)
Random rand = new Random();
int result = 0;
int count = 0;
for (int val : stream) {
count++;
if (rand.nextInt(count) == 0) {
result = val;
}
}
經典 LC: LC 382(Linked List Random Node)、LC 398(Random Pick Index)
Fisher-Yates 洗牌 — LC 384
// Time: O(N), Space: O(1) extra
void shuffle(int[] arr) {
Random rand = new Random();
for (int i = arr.length - 1; i > 0; i--) {
int j = rand.nextInt(i + 1);
int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
}
}
經典 LC: LC 384(Shuffle an Array)
加權隨機/在前綴和上二分搜尋 — LC 528
經典 LC: LC 528(Random Pick with Weight) — 每次抽取 O(log N)
Rejection Sampling — one uniform generator from another — LC 470 Priority 4 of 5 — High value — a gap here costs you rounds
Given rand5(), build rand7(). Two rules, and both are where candidates slip:
- Combine draws multiplicatively, never additively.
5 * rand5() + rand5()is uniform over0..24because the multiplier equals the second draw’s range, so each of the 25 outcomes comes from exactly one pair.rand5() + rand5()is not uniform — sums bunch in the middle. - Throw away the leftovers. 25 is not a multiple of 7, so
% 7alone would favour0..3. Keep only0..20(that is3 * 7) and re-draw otherwise.
// java
// CtCI 16.23 - rand7() from rand5(), where rand5() is uniform on 0..4
// IDEA: 5*rand5() + rand5() is uniform on 0..24; keep 0..20 (an exact multiple of 7)
// and take % 7, re-drawing otherwise so no residue is favoured
// time = O(1) expected — 25/21 ~= 1.19 rounds, space = O(1)
int rand7() {
while (true) {
int n = 5 * rand5() + rand5(); // uniform over 0..24
if (n < 21) return n % 7; // 21 = 3 * 7
}
}
# python
# LC 470 - Implement Rand10() Using Rand7() (rand7() is uniform on 1..7)
# IDEA: 7*(rand7()-1) + rand7() is uniform on 1..49; keep 1..40 = 4 * 10 and map
# with % 10; the rejected 9 values just cost another round
# time = O(1) expected — 49/40 = 1.225 rounds, space = O(1)
class Solution(object):
def rand10(self):
while True:
n = 7 * (rand7() - 1) + rand7() # uniform over 1..49
if n <= 40: # 40 is an exact multiple of 10
return 1 + (n - 1) % 10
The loop is unbounded but finishes fast: with success probability p per round the expected
number of rounds is 1/p — 49/40 here. Say that instead of “it usually terminates”.
Random Subset of Size M — partial Fisher-Yates
Picking m of n items uniformly does not need a full shuffle. Seed the result with the
first m items, then let each later item i replace a random slot with probability
m / (i+1) — which is reservoir sampling with k = m, and leaves every item at exactly
m / n.
# python
# CtCI 17.3 - a uniformly random subset of size m, in one pass over the input
# IDEA: reservoir sampling with k = m — item i (i >= m) lands in slot k when the
# random k < m, so early and late items end up equally likely
# time = O(n), space = O(m) — any iterable, and it never indexes or re-reads it
import random
def pick_m(items, m):
subset = []
for i, item in enumerate(items): # one pass; a generator is fine
if i < m:
subset.append(item) # seed the reservoir
else:
k = random.randint(0, i) # inclusive at both ends
if k < m:
subset[k] = item # replaces a slot with probability m/(i+1)
return subset # shorter than m if the input was
Classic LC: LC 384 uses the full shuffle above; this is the version to reach for when
m << n or the input is a stream you cannot shuffle in place.
Random Node of a Tree — weight by subtree size
Each node stores its subtree size, maintained on insert. “Give me a uniformly random node”
then becomes “give me the i-th node in order” for a uniform i — an O(h) descent.
# python
# CtCI 4.11 - getRandomNode(), every node equally likely
# IDEA: with sizes stored, picking the i-th node in order is a descent; a uniform i
# therefore makes every node equally likely. Same idea as LC 528, with the
# tree standing in for the prefix-sum array.
# time = O(h) per call, space = O(1) — sizes are maintained during insert
import random
def get_ith(node, i):
left_size = node.left.size if node.left else 0
if i < left_size:
return get_ith(node.left, i)
elif i == left_size:
return node
else:
return get_ith(node.right, i - left_size - 1)
def get_random(root):
return get_ith(root, random.randrange(root.size))
If you cannot change the node class, reservoir sampling over any traversal still gives a
uniform node in O(n) and O(1) extra space — the trade to name out loud is O(h) per call
with stored sizes versus O(n) per call without them.
模式 6:幾何/計算幾何
外積(方向判斷)
// Returns > 0 if counter-clockwise, < 0 if clockwise, 0 if collinear
long cross(int[] O, int[] A, int[] B) {
return (long)(A[0] - O[0]) * (B[1] - O[1])
- (long)(A[1] - O[1]) * (B[0] - O[0]);
}
凸包(Andrew 單調鏈) — LC 587
# Time: O(N log N), Space: O(N)
def convex_hull(points):
points.sort()
lower = []
for p in points:
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
lower.pop()
lower.append(p)
upper = []
for p in reversed(points):
while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
upper.pop()
upper.append(p)
return lower[:-1] + upper[:-1]
def cross(O, A, B):
return (A[0]-O[0])*(B[1]-O[1]) - (A[1]-O[1])*(B[0]-O[0])
距離與直線公式
Euclidean distance: sqrt((x2-x1)² + (y2-y1)²)
Manhattan distance: |x2-x1| + |y2-y1|
Tip: Avoid sqrt when possible — compare squared distances instead.
Tip: Use long to avoid integer overflow in distance calculations.
經典 LC:
- LC 149 Max Points on a Line — 用 GCD 表示斜率,O(N²)
- LC 587 Erect the Fence — 凸包
- LC 973 K Closest Points to Origin — O(N) quickselect 或 O(N log K) 堆積
- LC 1232 Check if Straight Line — 外積
變形:矩形上的排容原理 — LC 836/LC 223
轉折:這兩題都可以化約成同一個一維基本操作 — 兩個區間的重疊 — 每個軸各做一次。 重疊判定=「兩個軸都重疊」;聯集面積=排容原理。
Inclusion-Exclusion: |A ∪ B| = |A| + |B| - |A ∩ B|
(3 sets) |A∪B∪C| = Σ|A| - Σ|A∩B| + |A∩B∩C| ← signs alternate
1-D primitive: overlap(lo1,hi1, lo2,hi2) = max(0, min(hi1,hi2) - max(lo1,lo2))
2-D: axis-aligned rectangles are a PRODUCT of two independent intervals
=> area(A ∩ B) = overlapX * overlapY
// java
// LC 836 - Rectangle Overlap / LC 223 - Rectangle Area
// IDEA: one shared 1-D helper. Overlap <=> positive overlap on BOTH axes.
// Union area <=> inclusion-exclusion: |A| + |B| - |A ∩ B|.
// time = O(1), space = O(1)
long overlapLen(int lo1, int hi1, int lo2, int hi2) {
return Math.max(0, Math.min(hi1, hi2) - Math.max(lo1, lo2));
}
public boolean isRectangleOverlap(int[] a, int[] b) {
return overlapLen(a[0], a[2], b[0], b[2]) > 0 // x axis
&& overlapLen(a[1], a[3], b[1], b[3]) > 0; // y axis
}
public int computeArea(int ax1, int ay1, int ax2, int ay2,
int bx1, int by1, int bx2, int by2) {
long areaA = (long)(ax2 - ax1) * (ay2 - ay1);
long areaB = (long)(bx2 - bx1) * (by2 - by1);
long inter = overlapLen(ax1, ax2, bx1, bx2) * overlapLen(ay1, ay2, by1, by2);
return (int)(areaA + areaB - inter); // long math: coords reach 1e4
}
# python
# LC 836 - Rectangle Overlap / LC 223 - Rectangle Area
# time = O(1), space = O(1)
def overlap_len(lo1, hi1, lo2, hi2):
return max(0, min(hi1, hi2) - max(lo1, lo2))
def isRectangleOverlap(a, b):
return (overlap_len(a[0], a[2], b[0], b[2]) > 0
and overlap_len(a[1], a[3], b[1], b[3]) > 0)
def computeArea(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2):
areaA = (ax2 - ax1) * (ay2 - ay1)
areaB = (bx2 - bx1) * (by2 - by1)
inter = overlap_len(ax1, ax2, bx1, bx2) * overlap_len(ay1, ay2, by1, by2)
return areaA + areaB - inter
max(0, ...)才是安全的關鍵:負的原始重疊量必須夾成 0,否則兩個不相交的矩形會「扣掉」一塊不存在的正交集面積,把聯集撐大。
變形:用對角線配對數矩形 — LC 939
轉折:一個軸對齊矩形,被一條對角線唯一決定。所以列舉 C(N,2) 組點對,而不是 C(N,4) 組四元組,再用雜湊檢查另外兩個角。
// java
// LC 939 - Minimum Area Rectangle
// IDEA: pick 2 points as a DIAGONAL (needs different x AND different y), then the
// other two corners are forced: (x1,y2) and (x2,y1). Hash-set lookup, O(1).
// time = O(N^2), space = O(N)
public int minAreaRect(int[][] points) {
Set<Long> seen = new HashSet<>();
for (int[] p : points) seen.add(key(p[0], p[1]));
long best = Long.MAX_VALUE;
for (int i = 0; i < points.length; i++) {
for (int j = i + 1; j < points.length; j++) {
int x1 = points[i][0], y1 = points[i][1];
int x2 = points[j][0], y2 = points[j][1];
if (x1 == x2 || y1 == y2) continue; // same row/col -> not a diagonal
if (seen.contains(key(x1, y2)) && seen.contains(key(x2, y1))) {
best = Math.min(best, (long) Math.abs(x1 - x2) * Math.abs(y1 - y2));
}
}
}
return best == Long.MAX_VALUE ? 0 : (int) best;
}
private long key(int x, int y) { return (long) x * 40001 + y; } // coords <= 4*10^4
# python
# LC 939 - Minimum Area Rectangle
# time = O(N^2), space = O(N)
def minAreaRect(points):
seen = {(x, y) for x, y in points}
best = float("inf")
for i in range(len(points)):
x1, y1 = points[i]
for j in range(i + 1, len(points)):
x2, y2 = points[j]
if x1 == x2 or y1 == y2: # not a diagonal
continue
if (x1, y2) in seen and (x2, y1) in seen:
best = min(best, abs(x1 - x2) * abs(y1 - y2))
return 0 if best == float("inf") else best
每個矩形會被找到兩次(兩條對角線各一次)— 對取
min無害,但如果題目問的是矩形數量,記得除以 2。
模式 7:位元計數與數論
計算因數個數
Number of divisors of n = product of (e_i + 1) for each prime factor p_i^e_i
Example: 12 = 2² × 3¹ → (2+1)(1+1) = 6 divisors
歐拉函數
φ(n) = count of integers in [1, n] coprime to n
φ(p) = p - 1 for prime p
φ(p^k) = p^k - p^(k-1)
數位和/數位根 — LC 258
Digital root of n = 1 + (n-1) % 9 (for n > 0)
經典 LC: LC 258(Add Digits)、LC 1922(Count Good Numbers)
值得認得的具名定理(一句話版)
這些不需要模板 — 知道定理本身就等於解出來了。
| LC | 定理/技巧 | 好處 |
|---|---|---|
| 279 Perfect Squares | Lagrange 四平方和 ⇒ 答案 ∈ {1,2,3,4};當 n = 4^a(8b+7) 時是 4(Legendre 三平方和定理),n 是完全平方數時是 1,其餘再測 2 |
O(√N),取代 O(N√N) 的 DP |
| 633 Sum of Square Numbers | Fermat 二平方和:c = a²+b² 的充要條件是每個 ≡ 3 (mod 4) 的質因數指數皆為偶數;實作上直接用雙指標 a=0, b=⌊√c⌋ |
O(√C)、O(1) 空間 |
| 89 Gray Code | 反射二進位碼:g(i) = i ^ (i >> 1) — 由構造本身保證相鄰兩碼只差一個位元 |
輸出 O(2^n),不需要回溯 |
| 202 Happy Number | 數位平方和的映射,可達集合是有限的 ⇒ 必定循環(跟前面 LC 957 是同一套鴿籠論證);用 Floyd 龜兔賽跑可用 O(1) 空間偵測 | O(log N) 空間 → O(1) |
交叉參考(收在姊妹文件裡,這裡不重複)
- 數位/進位/進制運算 — LC 2、43、66、67、415、171 →
add_x_sum.md - 快速冪、GCD/LCM、整數開根號、進制轉換、隨機抽樣、伸縮級數 — LC 50、69、149、204、384、396、504、528、762 →
math.md
LC 範例
| # | 題目 | 模式 | 時間 | 空間 |
|---|---|---|---|---|
| 204 | Count Primes | 篩法 | O(N log log N) | O(N) |
| 96 | Unique BSTs | Catalan | O(N) | O(N) |
| 382 | Linked List Random Node | 蓄水池抽樣 | O(N) | O(1) |
| 528 | Random Pick with Weight | 前綴和 + 二分搜尋 | 建表 O(N),抽取 O(log N) | O(N) |
| 149 | Max Points on a Line | GCD 斜率 | O(N²) | O(N) |
| 587 | Erect the Fence | 凸包 | O(N log N) | O(N) |
| 878 | Nth Magical Number | LCM + 二分搜尋 | O(log(N·max(A,B))) | O(1) |
| 952 | Largest Component | 篩法 + 併查集 | O(N√M) | O(M) |
| 1071 | GCD of Strings | GCD | O(N+M) | O(1) |
| 384 | Shuffle an Array | Fisher-Yates | O(N) | O(1) |
| 372 | Super Pow | 模冪運算 | O(N) | O(1) |
| 1808 | Max Nice Divisors | 模冪 + 數學 | O(log N) | O(1) |
| 62 | Unique Paths | 格子路徑 → C(m+n-2, m-1) |
O(min(m,n)) | O(1) |
| 60 | Permutation Sequence | 階乘進位制(還原編號) | O(N²) | O(N) |
| 172 | Factorial Trailing Zeroes | Legendre 公式 | O(log₅ N) | O(1) |
| 477 | Total Hamming Distance | 依貢獻度計數(逐位元) | O(32·N) | O(1) |
| 523 | Continuous Subarray Sum | 前綴和模 K | O(N) | O(min(N,K)) |
| 957 | Prison Cells After N Days | 鴿籠原理 → 循環週期 | O(64·8) | O(64) |
| 223 | Rectangle Area | 排容原理 | O(1) | O(1) |
| 836 | Rectangle Overlap | 逐軸區間重疊 | O(1) | O(1) |
| 939 | Minimum Area Rectangle | 對角線配對 + 雜湊集合 | O(N²) | O(N) |