Advanced Binary Search
TL;DR
Advanced binary search extends beyond sorted arrays by identifying monotonic properties in problems. Search rotated arrays by finding pivot points. Find peak elements using monotonic sequences. Solve optimization problems (minimize maximum, capacity limits) by binary searching over answer ranges. Key insight: if you can verify a candidate answer in O(n), binary search can find the optimal answer in O(n log n) or better. Time complexity remains O(log n) comparisons regardless of problem complexity.
Core Concepts
Monotonic Property Recognition
The foundation of advanced binary search is identifying monotonic properties—relationships where adding more of something creates a consistent directional change in outcome.
Examples of monotonic properties:
- Rotated array: All elements left of pivot are >= all elements right of pivot
- Peak finding: Values increase then decrease (mountain shape)
- Capacity problems: If capacity X works, all capacities > X work (monotonic in feasibility)
- Resource allocation: More resources → better outcome (monotonic relationship)
How to identify: Ask "if X works, do all larger values work?" or "if X fails, do all smaller values fail?" If yes, you have monotonicity.
Search Space Abstraction
Instead of searching through array indices, advanced binary search searches through answer spaces:
- For "minimize the maximum load": search range [1, max_load]
- For "find peak element": search indices with mountain property
- For "split array": search possible split sizes
The invariant remains: at each step, eliminate half the search space by checking properties at the midpoint.
Finding Boundaries in Rotated Arrays
A rotated sorted array maintains local ordering properties. The key is identifying which half is properly sorted:
Original: [1,2,3,4,5,6,7]
Rotated: [4,5,6,7,1,2,3]
In the rotated array: left half [4,5,6,7] is sorted, right half [1,2,3] is sorted. The transition point (pivot) is where arr[i] > arr[i+1].
Key Algorithms
Search in Rotated Sorted Array
Problem: Find a target value in a rotated sorted array in O(log n) time.
Approach:
- Identify which half is properly sorted
- Determine if target is in sorted half
- Binary search on appropriate half
Key insight: One half is always sorted, allowing us to determine if target must be there.
Find Peak Element
Problem: Find element greater than its neighbors in array where arr[-1] and arr[len] are -∞.
Approach:
- Compare mid with neighbors
- Peak must be in direction of larger neighbor
- Continue narrowing search space
Key insight: A larger neighbor always leads toward peak due to problem constraints.
Minimize Maximum (Capacity to Ship Packages)
Problem: Given packages and days, find minimum capacity needed to ship all packages in given days, maintaining order.
Approach:
- Search range: [max_package_weight, sum_all_packages]
- For each candidate capacity, check if feasible
- Find minimum capacity that works
Key insight: Monotonicity: if capacity works, larger capacities work. Binary search over capacities, not indices.
Practical Example
- Python
- Java
- TypeScript
# Search in Rotated Sorted Array
def search_rotated(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
# Determine which half is sorted
if arr[left] <= arr[mid]: # Left half sorted
if arr[left] <= target < arr[mid]:
right = mid - 1 # Target in left half
else:
left = mid + 1 # Target in right half
else: # Right half sorted
if arr[mid] < target <= arr[right]:
left = mid + 1 # Target in right half
else:
right = mid - 1 # Target in left half
return -1
# Find Peak Element
def find_peak(arr):
left, right = 0, len(arr) - 1
while left < right:
mid = (left + right) // 2
if arr[mid] > arr[mid + 1]:
right = mid # Peak in left half (including mid)
else:
left = mid + 1 # Peak in right half
return left
# Capacity to Ship Packages in D Days
def ship_within_days(packages, days):
def can_ship(capacity):
current_load = 0
days_used = 1
for package in packages:
if current_load + package > capacity:
days_used += 1
current_load = package
else:
current_load += package
return days_used <= days
left = max(packages) # Min capacity: largest package
right = sum(packages) # Max capacity: all in one day
while left < right:
mid = (left + right) // 2
if can_ship(mid):
right = mid # Try smaller capacity
else:
left = mid + 1 # Need larger capacity
return left
# Minimize Maximum Load Across K Partitions
def minimize_max_load(weights, k):
def can_partition(max_load):
partitions = 1
current_sum = 0
for weight in weights:
if current_sum + weight > max_load:
partitions += 1
current_sum = weight
else:
current_sum += weight
return partitions <= k
left = max(weights) # Min possible max load
right = sum(weights) # Max possible max load
while left < right:
mid = (left + right) // 2
if can_partition(mid):
right = mid
else:
left = mid + 1
return left
# Test
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0)) # Output: 4
print(find_peak([1, 2, 3, 1])) # Output: 2
print(ship_within_days([1, 2, 3, 4, 5, 4, 1, 1, 4, 10], 5)) # Output: 15
print(minimize_max_load([1, 2, 3, 4, 5], 3)) # Output: 5
public class AdvancedBinarySearch {
// Search in Rotated Sorted Array
public static int searchRotated(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[left] <= arr[mid]) { // Left half sorted
if (arr[left] <= target && target < arr[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // Right half sorted
if (arr[mid] < target && target <= arr[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
// Find Peak Element
public static int findPeak(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > arr[mid + 1]) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// Ship Within Days
public static int shipWithinDays(int[] packages, int days) {
int left = 0, right = 0;
for (int p : packages) {
left = Math.max(left, p);
right += p;
}
while (left < right) {
int mid = left + (right - left) / 2;
if (canShip(packages, mid, days)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private static boolean canShip(int[] packages, int capacity, int days) {
int currentLoad = 0;
int daysUsed = 1;
for (int p : packages) {
if (currentLoad + p > capacity) {
daysUsed++;
currentLoad = p;
} else {
currentLoad += p;
}
}
return daysUsed <= days;
}
// Minimize Maximum Load Across K Partitions
public static int minimizeMaxLoad(int[] weights, int k) {
int left = 0, right = 0;
for (int w : weights) {
left = Math.max(left, w);
right += w;
}
while (left < right) {
int mid = left + (right - left) / 2;
if (canPartition(weights, mid, k)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private static boolean canPartition(int[] weights, int maxLoad, int k) {
int partitions = 1;
int currentSum = 0;
for (int w : weights) {
if (currentSum + w > maxLoad) {
partitions++;
currentSum = w;
} else {
currentSum += w;
}
}
return partitions <= k;
}
}
// Search in Rotated Sorted Array
function searchRotated(arr: number[], target: number): number {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[left] <= arr[mid]) { // Left half sorted
if (arr[left] <= target && target < arr[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // Right half sorted
if (arr[mid] < target && target <= arr[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
// Find Peak Element
function findPeak(arr: number[]): number {
let left = 0, right = arr.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] > arr[mid + 1]) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// Ship Within Days
function shipWithinDays(packages: number[], days: number): number {
let left = Math.max(...packages);
let right = packages.reduce((a, b) => a + b, 0);
const canShip = (capacity: number): boolean => {
let currentLoad = 0;
let daysUsed = 1;
for (const pkg of packages) {
if (currentLoad + pkg > capacity) {
daysUsed++;
currentLoad = pkg;
} else {
currentLoad += pkg;
}
}
return daysUsed <= days;
};
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (canShip(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// Minimize Maximum Load Across K Partitions
function minimizeMaxLoad(weights: number[], k: number): number {
let left = Math.max(...weights);
let right = weights.reduce((a, b) => a + b, 0);
const canPartition = (maxLoad: number): boolean => {
let partitions = 1;
let currentSum = 0;
for (const w of weights) {
if (currentSum + w > maxLoad) {
partitions++;
currentSum = w;
} else {
currentSum += w;
}
}
return partitions <= k;
};
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (canPartition(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
// Test
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 0)); // 4
console.log(findPeak([1, 2, 3, 1])); // 2
console.log(shipWithinDays([1, 2, 3, 4, 5, 4, 1, 1, 4, 10], 5)); // 15
console.log(minimizeMaxLoad([1, 2, 3, 4, 5], 3)); // 5
Common Pitfalls
Incorrect half detection in rotated arrays: Must verify with left boundary (arr[left]), not just relative to mid. Failing to check arr[left] <= arr[mid] leads to wrong half elimination.
Not accounting for duplicate values: In rotated arrays with duplicates, arr[left] == arr[mid] == arr[right] prevents determining sorted half. Requires shrinking boundaries to unique values.
Off-by-one errors on range bounds: For optimization problems, lower bound should be maximum element (not 1 or 0), upper bound should be sum. Wrong initialization fails to capture valid range.
Assuming monotonicity without verification: Not every "search" problem has binary searchable answer space. Verify that if candidate works, all larger candidates work (or vice versa).
Missing boundary checks for peak finding: Peak element must be compared against valid neighbors (handle edges where arr[-1] and arr[len] are -∞). Index out of bounds crashes.
Self-Check
- How do you determine which half of a rotated array is properly sorted? Why not just compare target with arr[mid]?
- Why does minimizing maximum load admit a binary search solution? What property must hold?
- Can binary search find a peak in O(log n) if we don't sort the array? Explain the monotonic property that makes this possible.
One Takeaway
Advanced binary search discovers hidden monotonic properties in non-obvious problems. The key breakthrough is recognizing that feasibility conditions (can capacity X work?) create a searchable answer space. By binary searching feasibility ranges instead of array indices, you solve complex optimization problems in O(log n) comparisons plus verification cost, achieving dramatic speedups over brute-force enumeration.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- LeetCode Problem 33: Search in Rotated Sorted Array
- LeetCode Problem 162: Find Peak Element
- LeetCode Problem 1011: Capacity to Ship Packages Within D Days