Divide and Conquer = “Chiến lược chia để trị” của Julius Caesar.
Đế chế La Mã quá lớn để quản lý trực tiếp:
Divide: chia đế chế thành các tỉnh nhỏ (subproblems)
Conquer: mỗi tỉnh tự trị độc lập (solve subproblems recursively)
Combine: tổng hợp kết quả từ các tỉnh thành quốc sách chung (merge results)
Key insight khác với DP: Các tỉnh KHÔNG chia sẻ tài nguyên (subproblems are INDEPENDENT). Mỗi tỉnh giải quyết vấn đề của chính nó mà không cần biết tỉnh khác làm gì.
D&C vs Dynamic Programming — sự khác biệt cốt lõi:
Divide & Conquer
Dynamic Programming
Subproblems
Independent (no overlap)
Overlapping (shared)
Reuse
Not needed
Memoization/tabulation
Examples
Merge Sort, Fast Power
Fibonacci, Knapsack
Structure
Tree of calls
DAG of states
FAANG Signal
D&C là nền tảng để hiểu DP (DP = D&C + memoization). Hiểu rõ D&C giúp bạn phân tích complexity qua Master Theorem và thiết kế algorithms như Merge Sort, Quick Sort, Fast Fourier Transform.
2. The FAANG Standard
Master Theorem — Complexity của D&C algorithms:
T(N)=a⋅T(N/b)+O(Nd)
Condition
Complexity
Example
d<logba
O(Nlogba)
Strassen matrix mult: O(N^2.81)
d=logba
O(NdlogN)
Merge Sort: O(N log N)
d>logba
O(Nd)
Binary Search: O(log N)
Ứng dụng Master Theorem:
Algorithm
Recurrence
Result
Merge Sort
T(N)=2T(N/2)+O(N)
O(NlogN)
Binary Search
T(N)=T(N/2)+O(1)
O(logN)
Fast Power
T(N)=T(N/2)+O(1)
O(logN)
Max Subarray D&C
T(N)=2T(N/2)+O(N)
O(NlogN)
Naive Matrix Mult
T(N)=8T(N/2)+O(N2)
O(N3)
3. Visual Thinking (Mermaid)
3.1 D&C Structure
graph TD
Root["PROBLEM(N)"] -->|"Divide"| Left["SUBPROBLEM(N/2)"]
Root -->|"Divide"| Right["SUBPROBLEM(N/2)"]
Left --> LL["SUBPROBLEM(N/4)"]
Left --> LR["SUBPROBLEM(N/4)"]
Right --> RL["SUBPROBLEM(N/4)"]
Right --> RR["SUBPROBLEM(N/4)"]
LL & LR -->|"Combine"| Left
RL & RR -->|"Combine"| Right
Left & Right -->|"Combine"| Root
style Root fill:#4a9eff,color:#fff
style Left fill:#8b5cf6,color:#fff
style Right fill:#8b5cf6,color:#fff
3.2 Merge Sort as D&C Tree
graph TD
A["[38, 27, 43, 3, 9, 82, 10]"] -->|"split"| B["[38, 27, 43, 3]"]
A -->|"split"| C["[9, 82, 10]"]
B -->|"split"| D["[38, 27]"]
B -->|"split"| E["[43, 3]"]
D -->|"split"| F["[38]"]
D -->|"split"| G["[27]"]
F & G -->|"merge"| H["[27, 38]"]
E -->|"split"| I["[43]"]
E -->|"split"| J["[3]"]
I & J -->|"merge"| K["[3, 43]"]
H & K -->|"merge"| L["[3, 27, 38, 43]"]
C -->|"..."| M["[9, 10, 82]"]
L & M -->|"merge"| N["[3, 9, 10, 27, 38, 43, 82]"]
style N fill:#22c55e,color:#fff
3.3 Closest Pair — Cross-midline Merge Step
flowchart LR
subgraph Left["Left Half"]
LP["Points sorted by X<br/>Min dist = d_L"]
end
subgraph Right["Right Half"]
RP["Points sorted by X<br/>Min dist = d_R"]
end
subgraph Strip["Strip Zone |x - mid| < d"]
SP["Check pairs in strip<br/>Only need O(7) comparisons per point!"]
end
Left -->|"d = min(d_L, d_R)"| Strip
Right --> Strip
Strip -->|"Combine"| Ans["Answer = min(d, strip_min)"]
style Ans fill:#22c55e,color:#fff
// Classic merge sort — the canonical D&C algorithmfunction mergeSort(arr: number[]): number[] { // Base case: array of 0 or 1 element is already sorted if (arr.length <= 1) return arr; // ── DIVIDE ──────────────────────────────────────────────────────────────── const mid = Math.floor(arr.length / 2); // ── CONQUER (independent recursive calls) ───────────────────────────────── const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); // ── COMBINE ─────────────────────────────────────────────────────────────── return merge(left, right);}function merge(left: number[], right: number[]): number[] { const result: number[] = []; let i = 0, j = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { result.push(left[i++]); } else { result.push(right[j++]); } } // Append remaining elements while (i < left.length) result.push(left[i++]); while (j < right.length) result.push(right[j++]); return result;}// T(N) = 2T(N/2) + O(N) → O(N log N) by Master Theorem
4.3 Maximum Subarray — Cross-Boundary Helper
// The COMBINE step for D&C max subarray// Find max subarray that crosses the midpoint// (extends from mid going left AND from mid+1 going right)function maxCrossingSubarray( nums: number[], lo: number, mid: number, hi: number): number { // Extend LEFT from mid let leftMax = -Infinity; let currentSum = 0; for (let i = mid; i >= lo; i--) { currentSum += nums[i]; leftMax = Math.max(leftMax, currentSum); } // Extend RIGHT from mid+1 let rightMax = -Infinity; currentSum = 0; for (let i = mid + 1; i <= hi; i++) { currentSum += nums[i]; rightMax = Math.max(rightMax, currentSum); } return leftMax + rightMax; // crossing subarray must include both sides}
4.4 Maximum Subarray D&C — O(N log N)
// LeetCode #53 — D&C approach// For each subarray, max is either:// (a) entirely in left half// (b) entirely in right half// (c) crossing the midpoint ← only this requires work in combine stepfunction maxSubarrayDC(nums: number[]): number { return dcHelper(nums, 0, nums.length - 1);}function dcHelper(nums: number[], lo: number, hi: number): number { // Base case if (lo === hi) return nums[lo]; // Divide const mid = lo + Math.floor((hi - lo) / 2); // Conquer const leftMax = dcHelper(nums, lo, mid); const rightMax = dcHelper(nums, mid + 1, hi); // Combine: check crossing const crossMax = maxCrossingSubarray(nums, lo, mid, hi); return Math.max(leftMax, rightMax, crossMax);}// T(N) = 2T(N/2) + O(N) → O(N log N)// Compare: Kadane's Algorithm = O(N) — D&C is WORSE here!// But D&C teaches the pattern used in: count inversions, closest pair, etc.
4.5 Count Inversions — D&C with Merge
// Count pairs (i, j) where i < j but nums[i] > nums[j]// Classic D&C: count inversions in left, right, and across boundaryfunction countInversions(nums: number[]): number { const result = mergeSortCount(nums.slice()); return result.count;}function mergeSortCount(arr: number[]): { sorted: number[]; count: number } { if (arr.length <= 1) return { sorted: arr, count: 0 }; const mid = Math.floor(arr.length / 2); const { sorted: left, count: leftCount } = mergeSortCount(arr.slice(0, mid)); const { sorted: right, count: rightCount } = mergeSortCount(arr.slice(mid)); // Merge and count cross-boundary inversions const merged: number[] = []; let crossCount = 0; let i = 0, j = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { merged.push(left[i++]); } else { // left[i] > right[j]: ALL remaining elements in left are inversions with right[j] crossCount += left.length - i; merged.push(right[j++]); } } while (i < left.length) merged.push(left[i++]); while (j < right.length) merged.push(right[j++]); return { sorted: merged, count: leftCount + rightCount + crossCount };}// T(N) = 2T(N/2) + O(N) → O(N log N) — same as merge sort// Naive: O(N²) — check all pairs
4.6 Fast Power — Exponentiation by Squaring
// LeetCode #50 — Pow(x, n)// Key insight: x^n = (x^(n/2))^2 if n is even// x^n = x * (x^(n/2))^2 if n is odd// Recurrence: T(N) = T(N/2) + O(1) → O(log N) by Master Theoremfunction myPow(x: number, n: number): number { // Handle negative exponent: x^(-n) = 1 / x^n if (n < 0) return 1 / myPow(x, -n); // Base cases if (n === 0) return 1; if (n === 1) return x; // ── DIVIDE ──────────────────────────────────────────────────────────────── const half = Math.floor(n / 2); // ── CONQUER ─────────────────────────────────────────────────────────────── const halfPow = myPow(x, half); // solve smaller problem // ── COMBINE ─────────────────────────────────────────────────────────────── if (n % 2 === 0) { return halfPow * halfPow; // x^n = (x^(n/2))^2 } else { return halfPow * halfPow * x; // x^n = x * (x^(n/2))^2 }}/*TRACE: myPow(2, 10)myPow(2, 10) = myPow(2, 5)^2myPow(2, 5) = 2 * myPow(2, 2)^2myPow(2, 2) = myPow(2, 1)^2myPow(2, 1) = 2 ← base caseUnrolling up:myPow(2, 2) = 2^2 = 4myPow(2, 5) = 2 * 4^2 = 2 * 16 = 32myPow(2, 10) = 32^2 = 1024 ✓Total recursive calls: O(log 10) ≈ 4 callsvs naive O(N=10) multiplications*/// Iterative version (avoids call stack overflow for very large n):function myPowIterative(x: number, n: number): number { if (n < 0) { x = 1 / x; n = -n; } let result = 1; let base = x; while (n > 0) { if (n % 2 === 1) result *= base; // if current bit is 1, multiply base *= base; // square the base n = Math.floor(n / 2); // shift right (process next bit) } return result;}
4.7 Majority Element — Boyer-Moore Voting
// LeetCode #169 — Majority Element// Problem: Find element appearing > N/2 times// Boyer-Moore Voting: O(N) time, O(1) spacefunction majorityElement(nums: number[]): number { let candidate = nums[0]; let count = 1; for (let i = 1; i < nums.length; i++) { if (count === 0) { // No current candidate — adopt nums[i] candidate = nums[i]; count = 1; } else if (nums[i] === candidate) { count++; // support current candidate } else { count--; // "cancel out" one vote for candidate } } return candidate; // majority element guaranteed to exist // WHY THIS WORKS: // Intuition: majority element M appears > N/2 times. // Every "cancellation" removes one M and one non-M. // Even after all non-M elements cancel M, M still has votes left. // The last standing candidate MUST be M.}/*TRACE for nums = [2, 2, 1, 1, 1, 2, 2]:i=0: candidate=2, count=1i=1: nums[1]=2 == candidate → count=2i=2: nums[2]=1 ≠ candidate → count=1i=3: nums[3]=1 ≠ candidate → count=0i=4: count=0 → candidate=1, count=1i=5: nums[5]=2 ≠ candidate=1 → count=0i=6: count=0 → candidate=2, count=1return 2 ✓ (2 appears 4 times > 7/2=3.5)NOTE: This is NOT D&C — it's a greedy/voting algorithm.D&C approach for majority exists but is O(N log N).Boyer-Moore is included here as the canonical O(N)/O(1) solution for FAANG.*/
Nếu chỉ divide rồi conquer mà không combine gì → thường chỉ là simple recursion
Max Subarray: combine step là maxCrossingSubarray — đây là phần “khó” nhất!
Off-by-One trong Mid Calculation
// Safe (no overflow, correct flooring):const mid = lo + Math.floor((hi - lo) / 2);// For merge sort with array indices:const mid = Math.floor((lo + hi) / 2); // fine for JS (no int overflow)// WRONG for large numbers (other languages):const mid = (lo + hi) / 2; // overflow in C++/Java if lo+hi > INT_MAX
Base Case Phải Dừng Recursion Đúng
Thiếu hoặc sai base case = infinite recursion (stack overflow).
// Wrong: never reaches base case if lo >= hi not handledif (lo === hi) return nums[lo]; // what about empty array (lo > hi)?// Correct: handle bothif (lo >= hi) return lo === hi ? nums[lo] : -Infinity;
D&C vs DP — Chọn Cái Nào?
Vẽ recursion tree: có overlapping subproblems không?
Fibonacci: fib(3) được tính nhiều lần → DP (memoize)
n có thể âm: x−n=1/xn.
Đặc biệt: n = -2147483648 (MIN_INT) → -n overflow trong C++/Java!
Trong JS/TS, numbers là float64 nên không có vấn đề này, nhưng vẫn nên aware.
7. Pattern Recognition
Khi Nào Dùng Divide & Conquer
“Count/find across a boundary” → D&C với merge step
→ Count inversions, closest pair of points, count range sum
“Sort / find k-th in sorted order” → D&C (merge sort, quick select)
→ Merge K sorted lists, nth element
“Fast exponentiation” → D&C squaring
→ x^n, matrix power (Fibonacci in O(log N)), mod exponentiation
“Optimal split into halves” → D&C with max/min combine
→ Max subarray D&C, closest pair
Karatsuba multiplication — multiply two N-digit numbers:
→ O(N1.585) thay vì O(N2) naive
Matrix exponentiation — Fibonacci trong O(logN):
→ [F(n+1)F(n)]=[1110]n[10]