Divide search space by half to find boundaries, handle rotations, or search on the answer
Maintain an invariant over a sorted or monotonic space; shrink [lo, hi] toward the boundary/target with careful mid updates.
Use on sorted arrays, unimodal functions, monotonic predicate checks, or optimization by 'binary search on answer'.
Find first true/last true index by steering toward boundary
def lower_bound(arr, target):
lo, hi = 0, len(arr)
while lo < hi:
mid = (lo + hi) // 2
if arr[mid] < target:
lo = mid + 1
else:
hi = mid
return lo # first index with arr[i] >= target
def upper_bound(arr, target):
lo, hi = 0, len(arr)
while lo < hi:
mid = (lo + hi) // 2
if arr[mid] <= target:
lo = mid + 1
else:
hi = mid
return lo # first index with arr[i] > target
Leverage one sorted half to guide search
def search_rotated(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
Binary search feasible answers using a monotonic check
def min_speed(piles, h):
# Koko Eating Bananas style feasibility check
import math
lo, hi = 1, max(piles)
def can(k):
return sum((p + k - 1) // k for p in piles) <= h
while lo < hi:
mid = (lo + hi) // 2
if can(mid):
hi = mid
else:
lo = mid + 1
return lo
def template(*args, **kwargs):
# General template placeholder
pass
Array search is O(log n); answer search is O(log R) checks times O(n) per check.
Iterative search uses constant space.