binary-search-in-action
::: info Required Knowledge
Before reading this article, you need to learn:
:::
In Binary Search Framework in Detail, we thoroughly examined the details of binary search, exploring three scenarios: "searching for an element", "searching for the left boundary", and "searching for the right boundary", teaching you how to write correct, bug-free binary search algorithms.
However, the binary search framework summarized in that article is limited to the basic scenario of "searching for a specified element in a sorted array". Real algorithm problems aren't this straightforward—you might not even recognize that a problem can use binary search.
So this article summarizes a framework for applying binary search algorithms, helping you think through and analyze binary search problems systematically, step by step, to arrive at the solution.
Original Binary Search Code
The prototype of binary search is searching for an element target in a sorted array and returning the corresponding index.
If the element doesn't exist, you can return some special value. These details can be implemented with minor adjustments to the algorithm.
There's another important issue: if the sorted array contains multiple target elements, these elements will definitely be adjacent. This involves whether the algorithm should return the index of the leftmost target element or the rightmost one—the so-called "searching for the left boundary" and "searching for the right boundary". This can also be implemented through minor code adjustments.
We discussed these issues in detail in the previous article Binary Search Core Framework. Readers who are unclear on this should review that article. If you already understand the basic binary search algorithm, you can continue reading.
In real algorithm problems, the "search left boundary" and "search right boundary" scenarios are commonly used, while rarely will you be asked to simply "search for an element".
This is because algorithm problems generally ask you to find optimal values, like finding the "minimum speed" for eating bananas or the "minimum capacity" for a ship. The process of finding optimal values is necessarily a process of searching for a boundary, so we'll analyze these two boundary search binary algorithms in detail.
::: note Note
Note that I'm using the left-closed, right-open notation for binary search. If you prefer both ends closed, you can modify the code accordingly.
:::
The specific code implementation for the "search left boundary" binary search algorithm is as follows:
Suppose the input array is nums = [1,2,3,3,3,5,7] and you want to search for target = 3. The algorithm will return index 2.
If we draw a diagram, it looks like this:
The specific code implementation for the "search right boundary" binary search algorithm is as follows:
With the same input, the algorithm will return index 4. If we draw a diagram, it looks like this:
Good, the above content is all review. I expect readers at this point should understand it all. Remember the diagrams above—any problem that can be abstracted into these diagrams can be solved using binary search.
Generalizing Binary Search Problems
What problems can apply binary search algorithm techniques?
First, you need to abstract from the problem an independent variable x, a function f(x) about x, and a target value target.
Additionally, x, f(x), target must satisfy the following conditions:
1. f(x) must be a monotonic function on x (either monotonically increasing or decreasing).
2. The problem asks you to calculate the value of x that satisfies the constraint f(x) == target.
These rules sound a bit abstract, so let's use a concrete example:
You're given a sorted array nums in ascending order and a target element target. Calculate the index position of target in the array. If there are multiple target elements, return the smallest index.
This is the "search left boundary" basic problem type. We've written the solution code before, but what are x, f(x), target here?
We can think of the array element indices as the independent variable x, and define the function relationship f(x) like this:
This function f is actually just accessing the array nums. Since the problem gives us the array nums in ascending order, the function f(x) is monotonically increasing on x.
Finally, what does the problem ask us to find? Doesn't it ask us to calculate the leftmost index of element target?
Isn't that equivalent to asking "what is the minimum value of x that satisfies f(x) == target"?
Draw a diagram like this:
If you encounter an algorithm problem and can abstract it into this diagram, you can apply binary search to it.
The algorithm code is as follows:
This code is actually redundant—it's the conventional binary search code with a slight modification, wrapping direct access to nums[mid] in a function f. However, this abstracts the framework of binary search thinking in specific algorithm problems.
A Framework for Applying Binary Search
When you want to use binary search to solve a specific algorithm problem, start by thinking through this code framework:
Here's how to break it down step by step:
1. Figure out what x, f(x), target represent, then write the code for function f.
2. Determine the range of x as your search interval, and initialize left and right accordingly.
3. Based on what the problem asks for, decide whether you need left-boundary or right-boundary binary search, then write your solution.
Let's walk through this process with a few examples.
Example 1: Koko Eating Bananas
This is LeetCode problem 875, "Koko Eating Bananas":
LeetCode 875. Koko Eating Bananas
Koko loves to eat bananas. There are n piles of bananas, the i^(th) pile has piles[i] bananas. The guards have gone and will come back in h hours.
Koko can decide her bananas-per-hour eating speed of k. Each hour, she chooses some pile of bananas and eats k bananas from that pile. If the pile has less than k bananas, she eats all of them instead and will not eat any more bananas during this hour.
Koko likes to eat slowly but still wants to finish eating all the bananas before the guards return.
Return the minimum integer k such that she can eat all the bananas within h hours.
Example 1:**
Example 2:**
Example 3:**
Constraints:
1 <= piles.length <= 10^(4)piles.length <= h <= 10^(9)1 <= piles[i] <= 10^(9)
Koko can eat at most one pile per hour. If she can't finish a pile, she'll continue eating it in the next hour. If she finishes early, she'll just wait until the next hour to start another pile.
She wants to finish all the bananas before the guard returns. We need to find the minimum eating speed K. Here's the function signature:
So how do we apply the framework we just outlined to write a binary search solution?
Just follow the steps:
1. Figure out what x, f(x), target represent, then write the code for function f.
What's the independent variable x? Think back to those function graphs—binary search is essentially searching over the independent variable.
Whatever the problem asks you to find, that's your x. Here, Koko's eating speed is our x.
What's the monotonic relationship f(x) with respect to x?
The faster Koko eats, the less time she needs to finish all the piles. Speed and time have a monotonic relationship.
So we can define f(x) like this:
If Koko eats at a speed of x bananas/hour, she needs f(x) hours to finish all the bananas.
Here's the implementation:
::: info Info
Why does f(x) return a long? Look at the data constraints and the logic of f. Elements in piles can be as large as 10^9, and there can be up to 10^4 elements. When x is 1, the hours variable could reach 10^13, which exceeds the maximum value of an int (roughly 2×10^9). Using long prevents potential integer overflow.
:::
The target is straightforward—the time limit H is naturally the target, serving as the maximum constraint on f(x)'s return value.
2. Determine the range of x as your search interval, and initialize left and right accordingly.
What's Koko's minimum eating speed? What's the maximum?
The minimum speed is obviously 1. The maximum is the largest element in piles, since she can eat at most one pile per hour—having a bigger appetite won't help.
You have two options here: either loop through piles to find the maximum value, or check the problem's constraints for the range of elements in piles and initialize right to a value just outside that range.
I'll go with the second approach. The problem states 1 <= piles[i] <= 10^9, so we can set our search boundaries like this:
Since binary search has logarithmic complexity, even if right is a huge number, the algorithm remains efficient.
3. Based on what the problem asks for, decide whether you need left-boundary or right-boundary binary search, then write your solution.
Now we know: x is the eating speed, f(x) is monotonically decreasing, and target is the time limit H. The problem asks for the minimum speed, meaning we want x to be as small as possible:
This calls for left-boundary binary search. But watch out—f(x) is monotonically decreasing, so don't blindly apply the template. Think it through with the diagram above:
::: tip Tip
I'm using the left-closed, right-open binary search style here. If you prefer the both-ends-closed style, just modify how you initialize right and update it:
For a deep dive into the details of this algorithm, check out Binary Search Explained. I won't go into it here.
:::
And that solves the problem. The extra if-branches in our framework are mainly there to help you understand what's happening. Once you've got a working solution, I'd recommend merging redundant branches to improve runtime efficiency:
Problem 2: Shipping Packages
Let's look at LeetCode problem 1011, Capacity To Ship Packages Within D Days:
LeetCode 1011. Capacity To Ship Packages Within D Days
A conveyor belt has packages that must be shipped from one port to another within days days.
The i^(th) package on the conveyor belt has a weight of weights[i]. Each day, we load the ship with packages on the conveyor belt (in the order given by weights). We may not load more weight than the maximum weight capacity of the ship.
Return the least weight capacity of the ship that will result in all the packages on the conveyor belt being shipped within days days.
Example 1:**
Example 2:**
Example 3:**
Constraints:
1 <= days <= weights.length <= 5 * 10^(4)1 <= weights[i] <= 500
You need to ship all packages in order within D days, and packages can't be split. What's the minimum capacity the ship needs?
Here's the function signature:
Same as before—just follow the process:
1. Figure out what x, f(x), target represent, then implement the function f.
The question asks about ship capacity, so that's our variable x.
Shipping days and capacity are inversely related—higher capacity means fewer days. So we'll make f(x) calculate how many days we need given capacity x. This makes f(x) monotonically decreasing.
Here's how to implement f(x):
For this problem, target is clearly the number of shipping days D. We need to find the minimum capacity where f(x) == D.
2. Determine the range of x to use as the binary search interval, and initialize left and right.
What's the minimum capacity? What's the maximum?
The minimum capacity should be the heaviest single package in weights—you've got to be able to carry at least one package per trip.
The maximum capacity would be the sum of all packages—just carry everything in one trip.
That gives us our search interval [left, right):
3. Based on what the problem asks for, decide whether to use left-boundary or right-boundary binary search, then write the solution.
Now we know: x is the ship's capacity, f(x) is a monotonically decreasing function, and target is the day limit D. The problem wants the minimum capacity, which means we want the smallest possible x:
This is a classic left-boundary binary search. Using the diagram above, we can write the code:
That's the solution! We can clean up the redundant if branches to make it faster. Here's the final code:
Problem 3: Split Array
Let's try LeetCode problem 410, Split Array Largest Sum, marked as Hard:
LeetCode 410. Split Array Largest Sum
Given an integer array nums and an integer k, split nums into k non-empty subarrays such that the largest sum of any subarray is minimized.
Return the minimized largest sum of the split.
A subarray is a contiguous part of the array.
Example 1:**
Example 2:**
Constraints:
1 <= nums.length <= 10000 <= nums[i] <= 10^(6)1 <= k <= min(50, nums.length)
Here's the function signature:
This problem is pretty confusing with all the max and min talk. Take a close look at the examples to understand what it's really asking.
Here's the gist: you're given an array nums and a number m, and you need to split nums into m subarrays.
There are multiple ways to split it. Each way creates m subarrays, and in each split, one of those subarrays will have the largest sum, right?
You want to find the split where that largest subarray sum is as small as possible compared to all other splits.
Your algorithm should return that minimized largest subarray sum.
Yikes, this sounds super hard and confusing. How do we even apply our binary search pattern here?
Here's the thing: this problem is actually identical to the shipping problem we just solved. Don't believe me? Let me rephrase it:
You have one cargo ship. You have several packages, each weighing nums[i]. You need to ship all of them within m days. What's the minimum capacity your ship needs?
That's exactly LeetCode 1011, Capacity To Ship Packages Within D Days, which we just solved!
Each day's cargo is one subarray of nums. Shipping everything in m days means splitting nums into m subarrays. Minimizing the ship's capacity means minimizing the largest subarray sum.
So you can literally copy-paste the shipping problem's solution:
That wraps it up! To summarize: if you spot a monotonic relationship in a problem, try using binary search. Once you understand the monotonicity and figure out which type of binary search to use, you can sketch it out and write the final code.
Last updated