Binary Search: The Meguru Method β

Why Traditional Binary Search Can Be Frustrating π« β
If youβve ever implemented binary search from a textbook, you know the struggle:
- Should it be
left <= rightorleft < right? - Do you return
-1,left,right,midor something else? - And why do off by one errors haunt you every time?
def binary_search(arr: list[int], target: int) -> int:
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid # Return inside the or outside loop?
elif arr[mid] < target:
left = mid + 1 # Add one or subtract one?
else:
right = mid - 1 # Can't remember tbh...
return -1 # Return what again?int binary_search(const std::vector<int>& arr, int target) {
int left = 0;
int right = static_cast<int>(arr.size()) - 1;
while (left <= right) {
int mid = std::midpoint(left, right);
if (arr[mid] == target) {
return mid; // Return inside the loop?
} else if (arr[mid] < target) {
left = mid + 1; // Add one here right?
} else {
right = mid - 1; // Can't remember tbh...
}
}
return -1; // What do we return again?
}Annoying, right? Letβs fix that.
A Better Way: Think in Terms of True and False π΅ β
Binary search operates on monotonic predicates[1]. Consider a predicate
When
FalseFalseFalseTrueTrueTrue
Or similarly:
TrueTrueTrueFalseFalseFalse
Binary search can find this transition boundary. Instead of juggling left and right indices and worrying about +1/-1, an easier strategy uses two pointers that converge toward the transition boundary through successive refinement.
Once the transition boundary is identified, we can select the True or False value depending on what the problem requires.
The Meguru Implementation π₯· β
The version popularized by Codeforces user Meguru does exactly this.
We initialize and maintain two pointers:
ok: a position already confirmed valid;ng: a position already confirmed invalid;
At each step:
- If
is true, set - Else, set
This rapidly shrinks the interval between ok and ng. Because one pointer is always valid and the other invalid, the search never overshoots or loses track of the boundary. The loop terminates when the two pointers are adjacent (for ok is returned.
def binary_search(arr: list[int], target: int) -> int:
ok: int = ... # confirmed valid
ng: int = ... # confirmed invalid
while abs(ok - ng) > 1:
mid: int = (ok + ng) // 2
if p(mid):
ok = mid # mid is valid
else:
ng = mid # mid is invalid
return okint binary_search(const std::vector<int>& arr, int target) {
int ok = ...; // confirmed valid
int ng = ...; // confirmed invalid
while (std::abs(ok - ng) > 1) {
int mid = std::midpoint(ok, ng);
if (p(mid)) {
ok = mid; // mid is valid
} else {
ng = mid; // mid is invalid
}
}
return ok;
}Note that neither ok nor ng are evaluated during the loop. At each step we only test the midpoint, which always satisfies
If the solution lies at index
or equivalently
Thus ok therefore yields the True position exactly at the boundary.
Remember βΌοΈ
ok always points to a position where the predicate is True, and ng always points to where it's False. When they're adjacent, we've found the boundary.
Why This Works π§ β
The invariant
Practice Problems π€ β
Start with these LeetCode problems to get comfortable with the pattern:
- 704. Binary Search
- 35. Search Insert Position
- 278. First Bad Version
- 875. Koko Eating Bananas
- 1011. Capacity To Ship Packages Within D Days
Once you understand this approach, you'll never struggle with binary search again.
References π β
CP-Algorithms. (2025, August 19). Binary search. Retrieved from https://cp-algorithms.com/num_methods/binary_search.html#search-on-arbitrary-predicate β©οΈ