1. description
Binary search the target in a sorted array or a solution for a monotonic problem.
C++ lowerbound and upperbound assume the array to be sorted in non-decreasing order.
Usually when doing binary search, you need to following the order of array. For example, if the array is in increasing order, we usually find the one that is greater than sth. It’s not possible to use binary search to find the one that is smaller than sth.
So it does not make sense when you perform lowerbound(arr.begin(), arr.end(), target) on an increasing array, which means find the first element that is >= target in an decreasing order array, which violate the searching order.
The lower_bound(arr.begin(), arr.end(), target, greater<int>()) in desendign order array, is used to find the first element <= target. upperbound is used to find the first element < target.
2. usage
2.1. asending ordered array
2.1.1. first greater or equal (lower bound)
lowerbound(nums.begin(), nums.end(), target); Three ways to write lower bound No matter how we represent the binary search window like in [left, right], [left, right) or (left, right). The core idea is shrink the window until the window is empty. (We checked all the elements if the window is empty). This is important to control the while loop condition.
2.1.1.1. [left, right], both side are closed
int search(vector<int>& nums, int target) { // [left, right] contains one possible answer, or no answer int left = 0, right = nums.size()-1; // when left -- right, [left, right] there is one element left, still need to check while(left <= right) { int mid = left + (right - left) / 2; if(nums[mid] < target) { left = mid + 1; // invariant: those from [0, left-1] are marked as red ==> not valid answer } else { right = mid - 1; // invariant: those from [right+1, end] are marked as blue ==> valid answer } } // after the loop, left must be in the right of right // we can return left of right + 1 return left; }
2.1.1.2. [left, right), left side is closed while right side is open
int search(vector<int>& nums, int target) { int left = 0, right = nums.size(); // if left == right, [left, right) there is no elements in the window, we processed all the elements and do not need to check while(left < right) { int mid = left + (right - left) / 2; if(nums[mid] < target) { left = mid + 1; // [0, left-1] is those < } else { right = mid; // [right, end] are those >= } } // after the loop, left will be the same as right, which has the answer // return left or right return right; }
2.1.1.3. (left, right), noth side are open
int search(vector<int>& nums, int target) { int left = -1, right = nums.size(); // when left+1 == right (left, right), there is no elements in the window, do not need to check while(left+1 < right) { int mid = left + (right - left) / 2; if(nums[mid] < target) { left = mid; // [0, left] are invalid } else { right = mid; // [right, end..] are valid } } // after the loop left+1 == right, we processed all the elements return right; }
2.1.2. first greater (upper bound)
upperbound(nums.begin(), nums.end(), target)
2.1.2.1. [left, right]
int search(vector<int>& nums, int target) { int left = 0, right = nums.size()-1; while(left<=right) { int mid = left + (right - left) / 2; if(nums[mid] <= target) { // <= will be ignored left = mid + 1; // [0, left-1] are marked as red } else { right = mid - 1; // [right+1, ..] are marked as blue } } // left is in the right of right pointer // left and right+1 are valid answer, return either one is ok return left; }
2.1.2.2. [left, right)
int search(vector<int>& nums, int target) { int left = 0, right = nums.size(); while(left < right) { int mid = left + (right - left) / 2; if(nums[mid] <= target) { left = mid + 1; // [0, left-1] are invalid } else { right = mid; // [right ... end] are valid } } // now left == right, we can return either of these return left; }
2.1.2.3. (left, right)
int search(vector<int>& nums, int target) { int left = -1, right = nums.size(); while(left+1 < right) { int mid = left + (right - left) / 2; if(nums[mid] <= target) { left = mid; // [0, left] are invalid } else { right = mid; // [right .. ] are valid } } // now left+1 == right, return left+1 and right are ok return right; }
2.1.3. last smaller or equal
upperbound(nums.begin(), nums.end(), target) - 1
2.1.3.1. [left, right]
int search(vector<int>& nums, int target) { int left = 0, right = nums.size()-1; while(left <= right) { int mid = left + (right - left) / 2; if(nums[mid] > target) { right = mid - 1; // [right+1 ... end] are invalid } else { left = mid + 1; // [0...left-1] are valid } } // now left is in the right of right // return left-1 or right is valid, return any is ok return right; }
2.1.3.2. [left, right)
int search(vector<int>& nums, int target) { int left = 0, right = nums.size(); while(left < right) { int mid = left + (right - left) / 2; if(nums[mid] > target) { right = mid; // [right,...] are invalid } else { left = mid + 1; // [0 ... left-1] are valid } } // now left is in the right of left == right // left-1 and right-1 are valid, return either is ok return left - 1; }
2.1.3.3. (left, right)
int search(vector<int>& nums, int target) { int left = -1, right = nums.size(); while(left+1 < right) { int mid = left + (right - left) / 2; if(nums[mid] > target) { right = mid; // [right end] are invalid } else { left = mid; // [0..left] are valid } } // now left+1 == right // left and right - 1 are valid, return anyone is ok return left; }
2.1.4. last smaller
lowerbound(nums.begin(), nums.end(), target) - 1
2.1.4.1. [left, right]
int search(vector<int>& nums, int target) { int left = 0, right = nums.size()-1; while(left<=right) { int mid = left + (right - left) / 2; if(nums[mid] >= target) { right = mid - 1; // [right+1 .. ] are invalid } else { left = mid + 1; // [0 ... left-1] are valid } } // now left is in the right of left // left -1 and right are valid, we can return either return right; }
2.1.4.2. [left, right)
int search(vector<int>& nums, int target) { int left = 0, right = nums.size(); while(left < right) { int mid = left + (right - left) / 2; if(nums[mid] >= target) { right = mid; // [right ... ] are invalid } else { left = mid + 1; // [0 .. left-1] are valid } } // now left == right // left - 1 or right - 1 are valid return left - 1; }
(left, right)
int search(vector<int>& nums, int target) { int left = -1, right = nums.size(); while(left+1 < right) { int mid = left + (right - left) / 2; if(nums[mid] >= target) { right = mid; // [right ... ] are invalid } else { left = mid; // [0...left] are valid } } // now left+1 = right // left and right - 1 are valid, return any is ok return left; }
2.1.5. first smaller (does not make sense to use binary search)
2.1.6. first smaller or equal (does not make sense to use binary search)
2.2. desending ordered array
for desending order, we need to overwrite the comparator operator
2.2.1. first smaller or equal (lower bound)
lowerbound(nums.begin(), nums.end(), target, greater<int>())
2.2.2. first smaller (upper bound)
upperbound(nums.begin(), nums.end(), target, greater<int>())
2.2.3. last greater or eqaul
upperbound(nums.begin(), nums.end(), greater<int>()) - 1
2.2.4. last greater
lowerbound(nums.begin(), nums.end(), greater<int>)
2.2.5. first greater (does not make sense to use binary search)
2.2.6. first greater or equal (does not make sense to use binary search)
2.3. paramatric search
Parametric Search is a technique where you use binary search to find the smallest or largest value (parameter) that satisfies a given condition — even when the value itself isn’t in a list or array. For example, you want find the largest value k that can be used to finish something. You can one helper function isok(k), which returns true if k can be used to finish that thing
(left, right)
int search() { int left = -1 (or other impossible value), right = INT_MAX (or other impossible value); while(left+1 < right) { int mid = left + (left - right) / 2; if(!is_ok(mid)) { right = mid; // [right ... ] are invalid } else { left = mid // [0.. left] are valid } } // now left+1 == right // left and right - 1 is valid, return either is ok return left; }