Comprehensive theory, algorithmic patterns, templates, and problem catalog for Binary Search and Search on Answer space.
Binary Search reduces the search space by half at each step, yielding logarithmic
-
Prerequisite: Monotonicity (the search space or a predicate function evaluates to
[true, true, ..., false, false]or monotonic increasing values). -
Core Principle: Find the boundary or target by querying the midpoint
$M = L + \frac{R - L}{2}$ .
- Index Space: Direct lookup in a sorted/rotated array or matrix.
- Value / Answer Space: When minimizing the maximum (or maximizing the minimum) feasible answer (e.g. Koko Eating Bananas, Capacity To Ship Packages).
// Lower Bound (First index where nums[i] >= target)
int lowerBound(const vector<int>& nums, int target) {
int left = 0, right = nums.size(); // [left, right)
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}int searchRotated(vector<int>& nums, int target) {
int left = 0, right = static_cast<int>(nums.size()) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
// Check if left half is sorted
if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// Right half must be sorted
else {
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}// Example: Koko Eating Bananas
bool isPossible(const vector<int>& piles, int speed, int h) {
long long hours = 0;
for (int pile : piles) {
hours += (pile + speed - 1) / speed; // Ceiling division
}
return hours <= h;
}
int minEatingSpeed(vector<int>& piles, int h) {
int low = 1, high = *max_element(piles.begin(), piles.end());
int result = high;
while (low <= high) {
int mid = low + (high - low) / 2;
if (isPossible(piles, mid, h)) {
result = mid; // Try finding a smaller feasible speed
high = mid - 1;
} else {
low = mid + 1; // Speed too slow, increase speed
}
}
return result;
}When range queries
first = lower_bound(blockEnds.begin(), blockEnds.end(), L) - blockEnds.begin()-
last = upper_bound(blockStarts.begin(), blockStarts.end(), R) - blockStarts.begin() - 1Boundary blocks$[first]$ and$[last]$ are clamped to$[L, R]$ , while fully contained interior blocks$[first+1, last-2]$ are queried in$\mathcal{O}(1)$ via a precomputed Sparse Table or Segment Tree.
When finding the minimum element in a rotated sorted array with duplicates:
- Compare
nums[mid]againstnums[right]. - If
nums[mid] > nums[right]: pivot is to the right$\to$ left = mid + 1. - If
nums[mid] < nums[right]: right half is sorted$\to$ right = mid. - If
nums[mid] == nums[right]: ambiguous inflection point$\to$ safely decrementright--($\mathcal{O}(\log N)$ average,$\mathcal{O}(N)$ worst).
When maximizing the minimum distance among
-
Linearize 2D Boundary to 1D: Map
$(x, y) \to pos \in [0, 4 \times side)$ . -
Binary Search on Feasible Distance: Search for optimal minimum distance
$D \in [1, side]$ . -
Pigeonhole Pruning: Test candidate starting points within
$[pos[0], pos[0] + P/k]$ . -
Greedy Jumps with
std::lower_bound: Select$k - 1$ subsequent points on the doubled array$pos + P$ in$\mathcal{O}(k \log N)$ time.
To find the median or
-
Partition Search Space: Ensure
$M \le N$ . Binary search$i \in [0, M]$ elements taken from array 1, fixing$j = \lfloor \frac{M + N + 1}{2} \rfloor - i$ elements from array 2. -
Boundary Sentinels: Define
$\text{maxLeft1} = (i == 0) , ? , -\infty : A[i-1]$ ,$\text{minRight1} = (i == M) , ? , +\infty : A[i]$ , and similarly for$B$ .
To find the minimum element or rotation pivot when the array contains duplicate elements:
- Maintain search window
[left, right]withmid = left + (right - left) / 2. - Compare
nums[mid]againstnums[right]:- If
nums[mid] > nums[right]: The inflection pivot must lie strictly in the right half$\implies \text{left} = \text{mid} + 1$ . - If
nums[mid] < nums[right]: The right half is strictly sorted; minimum lies in the left half or atmid$\implies \text{right} = \text{mid}$ . - If
nums[mid] == nums[right]: Ambiguity exists due to duplicates; safely eliminate the redundant boundary element without missing the minimum$\implies \text{right} = \text{right} - 1$ .
- If
- Average time complexity is
$\mathcal{O}(\log N)$ , degrading to$\mathcal{O}(N)$ in the worst case when all elements are identical.
When counting index pairs
-
Divide & Conquer on Prefix Sums: Recursively divide the prefix array into
$[ \text{left}, \text{mid} ]$ and$[ \text{mid} + 1, \text{right} ]$ , sorting each half during the merge step. -
Monotonic Window Traversal: For each
$j \in [\text{mid} + 1, \text{right}]$ , both target bounds$[P[j] - \text{upper}, P[j] - \text{lower}]$ increase monotonically. -
Linear Cross-Counting: Advance two non-resetting pointers
low_ptrandhigh_ptrthrough the sorted left half in$\mathcal{O}(\text{right} - \text{left} + 1)$ time. -
Merge: In-place / buffer merge maintains sorted order for the parent recursion frame, achieving optimal
$\mathcal{O}(N \log N)$ total time without coordinate compression overhead.
When finding the max rectangle sum
-
Column Compression: Fix left column
$l$ and right column$r$ . MaintainrowSum[i]= sum ofmatrix[i][l..r], reducing the 2D problem to a 1D constrained max subarray sum. -
Ordered Set for Constrained Prefix Sum: For prefix sum
currSum, find the smallestprevSum$\ge$ currSum - kusingstd::set::lower_bound(currSum - k)in$\mathcal{O}(\log m)$ . -
Dimension Optimization: Always iterate over
$\min(m, n)^2$ pairs in the outer loop. Transpose the matrix if$m < n$ . -
Total Complexity:
$\mathcal{O}(\min(m,n)^2 \cdot \max(m,n) \cdot \log(\max(m,n)))$ .
When partitioning a contiguous array into
-
Monotonicity Identification: Feasibility predicate
$P(S)$ = "Cannumsbe split into$\le k$ subarrays with sum$\le S$ ?" evaluates monotonically as[false, ..., false, true, ..., true]. -
Search Range Bounds:
-
$\text{low} = \max(\text{nums})$ (the single largest element cannot be subdivided). -
$\text{high} = \sum \text{nums}$ (all elements in a single partition).
-
-
Linear Greedy Verification: Greedily accumulate elements into the current subarray; as soon as adding the next element exceeds
$S$ , start a new subarray. If total subarrays exceed$k$ , returnfalse. -
Logarithmic Convergence: Achieves
$\mathcal{O}(N \log(\sum \text{nums}))$ time with$\mathcal{O}(1)$ space, drastically outperforming$\mathcal{O}(k N^2)$ DP.
When counting index pairs
-
Divide & Conquer: Recursively sort
$[l, mid]$ and$[mid+1, r]$ . -
Two-Pointer Monotonic Cross Counting: For each
$i \in [l, mid]$ , advance right pointer$j \in [mid+1, r]$ while$\text{nums}[i] > c \times \text{nums}[j]$ . Add$(j - (mid + 1))$ to the inversion count in$\mathcal{O}(r - l + 1)$ amortized time. -
64-bit Overflow Protection: Evaluate
$c \times \text{nums}[j]$ using2LL * nums[j]to avoid signed 32-bit integer overflow. -
Complexity:
$\mathcal{O}(N \log N)$ time and$\mathcal{O}(N)$ space.
Pattern M: Search on Answer in Implicit Monotonic 2D Matrices (Kth Smallest Number in Multiplication Table)
When finding the
-
Search Domain: Low bound
$1$ , high bound$M \times N$ . -
Row-Wise Counting Predicate: For candidate
$X$ , row$i$ values are$i \times 1, i \times 2, \dots, i \times N$ . The number of elements$\le X$ in row$i$ is$\min(N, \lfloor X / i \rfloor)$ . -
Dimension Optimization: Summing over
$\min(M, N)$ rows evaluates$f(X)$ in$\mathcal{O}(\min(M, N))$ time. -
Complexity:
$\mathcal{O}(\min(M, N) \cdot \log(M \cdot N))$ time and$\mathcal{O}(1)$ space.
-
Integer Overflow in Midpoint Calculation: Always use
mid = low + (high - low) / 2instead of(low + high) / 2. -
Off-by-One in Boundary Conditions:
-
while (low <= high)requireslow = mid + 1andhigh = mid - 1. -
while (low < high)requiresright = midorleft = mid + 1.
-
-
Duplicates in Rotated Array: If
nums[left] == nums[mid] == nums[right], we cannot determine which half is sorted; we must shrink bounds withright--($\mathcal{O}(N)$ worst-case). -
64-bit Range Queries on Prefix Sums: When calculating
$P[j] - \text{upper}$ , 32-bit values can overflow; always uselong longfor prefix sums and interval checks.
| # | Title | Difficulty | Time | Space | Solution Link |
|---|---|---|---|---|---|
| 4 | Median of Two Sorted Arrays | Hard |
C++ | ||
| 154 | Find Minimum in Rotated Sorted Array II | Hard |
|
C++ | |
| 315 | Count of Smaller Numbers After Self | Hard |
C++ | ||
| 327 | Count of Range Sum | Hard |
C++ | ||
| 363 | Max Sum of Rectangle No Larger Than K | Hard |
C++ | ||
| 410 | Split Array Largest Sum | Hard |
C++ | ||
| 493 | Reverse Pairs | Hard |
C++ | ||
| 668 | Kth Smallest Number in Multiplication Table | Hard |
C++ | ||
| 719 | Find K-th Smallest Pair Distance | Hard |
C++ | ||
| 793 | Preimage Size of Factorial Zeroes Function | Hard |
C++ | ||
| 878 | Nth Magical Number | Hard |
C++ | ||
| 887 | Super Egg Drop | Hard |
C++ | ||
| 3312 | Sorted GCD Pair Queries | Hard |
C++ | ||
| 3464 | Maximize the Distance Between Points on a Square | Hard |
C++ | ||
| 3501 | Maximize Active Section with Trade II | Hard |
C++ | ||
| 3620 | Network Recovery Pathways | Hard |
C++ |