Binary Search
- The idea is quite simple but difficult to write bug-free implementation code in just a few minutes
- When to exit the loop? Should we use
left < right
orleft <= right
as the while loop condition? - How to initialize the boundary variable
left
andright
? - How to update the boundary? How to choose the appropriate combination from
left= mid
,left = mid + 1
, andrigth = mid
,right = mid - 1
- When to exit the loop? Should we use
- Another misunderstanding of binary search is that people often think this technique could only be used in simple scenario like given a sorted array, find a specific value in it => Can be applied to much more complicated situations.
Idea
- Most of the time, the problem is not just find a target number in a sorted array. In this case the sorted array is a search space and target number is the search target. In some problem the search space and search target is not so readily available. => Have to correctly find and declare the search space and search target for binary search to work.
- When can we use binary search? => If we can discover some kind of monotonicity, e.g if condition(k) is True then condition(k+1) is True then we can consider binary search
- To stop thinking about binary search, we can think of these problems as Finding the smallest number that satisfy a condition
Template
def binary_search(array) -> int:
def condition(value) -> bool:
pass
left, right = min(search_space), max(search_space) # could be [0, n], [1, n] etc. Depends on problem
while left < right:
mid = left + (right - left) // 2
if condition(mid):
right = mid
else:
left = mid + 1
return left
- When using this template, we only need to modify three parts
- Correctly initialize
left
andright
to specify search space. One rule is to setup the boundary to include all possible elements - Decide return values. Is it
return left
orreturn left - 1
? Remember this: after exiting the while loop,left
**is the minimal k satisfying thecondition
function. - Design
condition
function.
- Correctly initialize
Example
278. First Bad Version [Easy]
You are a product manager and currently leading a team to develop a new product. Since each version is developed based on the previous version, all the versions after a bad version are also bad. Suppose you have n
versions [1, 2, ..., n]
and you want to find out the first bad one, which causes all the following ones to be bad. You are given an API bool isBadVersion(version)
which will return whether version
is bad.
Example:
Given n = 5, and version = 4 is the first bad version.
call isBadVersion(3) -> false
call isBadVersion(5) -> true
call isBadVersion(4) -> true
Then 4 is the first bad version.
First, we initialize left = 1
and right = n
to include all possible values. Then we notice that we don't even need to design the condition
function. It's already given by the isBadVersion
API. Finding the first bad version is equivalent to finding the minimal k satisfying isBadVersion(k) is True
. Our template can fit in very nicely:
def firstBadVersion(self, n) -> int:
left, right = 1, n
while left < right:
mid = left + (right - left) // 2
if isBadVersion(mid):
right = mid
else:
left = mid + 1
return left
https://leetcode.com/problems/capacity-to-ship-packages-within-d-days/
- Idea:
- We are given an array of weights and a day limit
- We might automatically treat weights as search space
- The way to look at this problem is
- We are looking at the minimal one among many feasible capacities. If we can successfully ship all packages within
D
days with capacitym
, then we can definitely ship them all with any capacity larger thanm
. - We can design a
condition
function, given an inputcapacity
, it returns whether it's possible to ship all packages withinD
days. This is just a regular function with a for loop. - In this case the search space is the
capacity
, where minimum ismax(weights)
as it has to be able to ship the heaviest package, and maximum issum(weights)
as if the capacity issum(weight)
we can ship all packages in one day.
- We are looking at the minimal one among many feasible capacities. If we can successfully ship all packages within
- Now we can apply the template
def shipWithinDays(weights: List[int], D: int) -> int:
def feasible(capacity) -> bool:
days = 1
total = 0
for weight in weights:
total += weight
if total > capacity: # too heavy, wait for the next day
total = weight
days += 1
if days > D: # cannot ship within D days
return False
return True
left, right = max(weights), sum(weights)
while left < right:
mid = left + (right - left) // 2
if feasible(mid):
right = mid
else:
left = mid + 1
return left
[[Blind 75-150#^e3a2b2 | Koko eating bananas]]
https://leetcode.com/problems/minimum-limit-of-balls-in-a-bag
- Search max size of each bags
- At each
mid
value, check if we can split every bag to themid
value withinmaxOperations
def minimumSize(self, nums: List[int], maxOperations: int) -> int:
def split(size):
count = 0
for num in nums:
count += num // size
# split 9 to 3,3,3 take 2 split instead of 3
if num % size == 0:
count -= 1
return count <= maxOperations
# search the max size of the bags
left, right = 1, max(nums)
while left < right:
mid = left + (right-left)//2
if split(mid):
right = mid
else:
left = mid + 1
return left
https://leetcode.com/problems/minimized-maximum-of-products-distributed-to-any-store
def minimizedMaximum(self, n: int, quantities: List[int]) -> int:
def canSplit(num):
count = 0
for q in quantities:
count += math.ceil(q / num)
return count
# search over number of products each store can have
left, right = 1, max(quantities)
while left < right:
mid = left + (right-left)//2
count = canSplit(mid)
# if we can distribute with current num of product, maybe we can push for it and do less?
if count <= n:
right = mid
# the num of product is too little => have excess products
else:
left = mid + 1
return left
https://leetcode.com/problems/maximum-candies-allocated-to-k-children/description/
- Similar intuition
def maximumCandies(self, candies: List[int], k: int) -> int:
def canAllocate(num):
count = 0
for c in candies:
count += c // num
return count >= k
left, right = 1, max(candies)
res = 0
while left <= right:
mid = left + (right - left)//2
if canAllocate(mid):
res = max(res, mid)
left = mid + 1
else:
right = mid - 1
return res
Tips:
When implementing binary search, choosing between left <= right vs left < right or using right = mid vs right = mid - 1 depends on:
Inclusivity of Bounds: • left <= right: Used when right is a valid candidate for the answer. • left < right: Used when right is not a valid candidate, so the range [left, right] reduces to [left, mid) or (mid, right].
Mid Calculation: • Always calculate mid = left + (right - left) // 2 to avoid overflow.
Shrinking the Range: • right = mid - 1: Excludes mid from the search space, typically when mid has already been validated as not meeting the condition. • right = mid: Includes mid in the search space if mid is a possible answer.
Tricks:
- If we want to include the
right
value as a candidate to the answer and avoid infinite loop- Do
left <= right
and when shrinking the range, doright = mid - 1
=> ifmid
is a valid answer, we already account for it by theleft == right
case - Do
left < right
and when shrinking the range, doright = mid
, **but we have to initializeright = max(searchSpace) + 1
so that we include it by default
- Do
https://leetcode.com/problems/magnetic-force-between-two-balls
- This is also a good one
def maxDistance(self, position: List[int], m: int) -> int:
position = sorted(position)
def canDistribute(force):
count = 1
pos = position[0]
for i in range(1, len(position)):
if position[i] - pos >= force:
count += 1
pos = position[i]
return count >= m
left, right = 0, position[-1] - position[0]
# left <= right because right value is also a valid answer
while left <= right:
mid = left + (right-left)//2
# exclude mid from the search
if canDistribute(mid):
left = mid + 1
else:
right = mid - 1
return left - 1 # handle left == right in the last iteration
https://leetcode.com/problems/search-in-rotated-sorted-array/description/
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right-left)//2
if nums[mid] == target:
return mid
# Case 2: subarray on mid's left is sorted
elif nums[mid] >= nums[left]:
if target >= nums[left] and target < nums[mid]:
right = mid - 1
else:
left = mid + 1
# Case 3: subarray on mid's right is sorted.
else:
if target <= nums[right] and target > nums[mid]:
left = mid + 1
else:
right = mid - 1
return -1
https://leetcode.com/problems/find-minimum-in-rotated-sorted-array
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right-left)//2
if nums[mid] >= nums[right]:
left = mid + 1
else:
right = mid
return nums[left-1]