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'.
Three canonical forms: index search, lower/upper bound, and binary search on answer with a monotone feasibility predicate.
# Binary Search Templates
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
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
def binary_search_answer(lo, hi, feasible):
# feasible(x) returns True/False, monotonic in x
while lo < hi:
mid = (lo + hi)//2
if feasible(mid):
hi = mid
else:
lo = mid + 1
return loFind 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] > targetLeverage 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 -1Binary 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 loBattle-tested templates and off-by-one edge cases for boundaries across languages
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] > targetArray search is O(log n); answer search is O(log R) checks times O(n) per check.
Iterative search uses constant space.