BS-14. Find the Smallest Divisor Given a Threshold | Binary Search
This chapter explores how to find the optimal positive integer divisor such that the sum of the ceiling values of array elements divided by that divisor is less than or equal to a given threshold. We will progress from a naive linear search to an optimal Binary Search on Answers solution.
1. Problem Statement & Core Objective
Given an array of integers and an integer threshold value, our task is to find the smallest divisor such that when every element in the array is divided by this divisor, and the individual ceiling values of each division are summed up, the total sum is less than or equal to the threshold.
Mathematical Formulation
For an array and a divisor , we calculate:
We need to find the minimum such that:
2. Real-Life Mental Model & Analogy
Real-Life Analogy: The Portion Cutter
Imagine you have a pile of heavy stone blocks of varying weights (represented by the array elements). You want to pack these blocks into smaller shipping containers of capacity . Because fractional containers aren't allowed, any leftover fraction of a block requires an entirely new container (hence the ceiling function).
- If your container size is very small (e.g., ), every block gets its own container (or multiple containers), leading to a huge total count that exceeds your shipping limit (threshold).
- If your container size is very large, everything fits into fewer containers than your limit.
- Your goal is to find the smallest container size that keeps the total number of containers under or equal to your threshold limit.
3. Working Example Walkthrough
Consider:
- Array
- Threshold
Let's test different divisors ():
Among all valid divisors (), the smallest divisor is (or depending on array values and threshold constraints; let's verify with the transcript's specific threshold walkthrough where threshold , divisor gives sum , which is ).
4. Naive Linear Search Approach
Status Quo & Logic
- We know the divisor cannot be or negative. Positive divisors start from .
- What is the maximum possible divisor we ever need to check? The maximum element in the array (
Max). Dividing any element by a number larger than itself results in , whose ceiling is1. Thus, pushing the divisor beyond the maximum array element yields the minimum possible sum (which equals the size of the array, ). - Therefore, our search space for the divisor is strictly bounded between
1andMax(Array).
Naive Implementation Code
int smallestDivisor(vector<int>& nums, int threshold) {
int maxElement = *max_element(nums.begin(), nums.end());
for (int d = 1; d <= maxElement; d++) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
sum += ceil((double)(nums[i]) / d);
}
if (sum <= threshold) {
return d; // First valid divisor found is the smallest
}
}
return -1;
}
Complexity Analysis
- Time Complexity: , where is the maximum element in the array and is the number of elements. If is large (e.g., ), this approach will result in a Time Limit Exceeded (TLE) error.
- Space Complexity: auxiliary space.
5. Transitioning to Binary Search on Answers
Why Binary Search Applies
- Monotonic Range: We are looking for a minimum value within a known range .
- Monotonic Behavior:
- If a divisor is too small, the division sums will be too large, violating the threshold.
- If we increase the divisor, the sum strictly decreases or remains constant (monotonic property).
- Once a divisor is valid, all larger divisors are also valid. Once a divisor is invalid, all smaller divisors are also invalid.
Divisor: 1 2 3 4 5 6 7 8 9 (Max)
Validity: No No Yes Yes Yes Yes Yes Yes Yes
(Invalid Zone) | (Valid Zone)
v
Smallest Valid (Our Target)
6. Step-by-Step Binary Search Execution Flow
-
Initialize Pointers:
low = 1high = Max(Array)ans = -1
-
Compute Mid:
mid = low + (high - low) / 2
-
Evaluate Sum at
mid:- Helper function computes .
-
Adjust Search Space:
- If : This
midis a valid candidate. Storeans = mid, and since we want the smallest divisor, eliminate the right half (high = mid - 1). - If : This
midis invalid. We need a larger divisor, so eliminate the left half (low = mid + 1).
- If : This
7. Complete Optimized Code Implementation
Here is the robust, production-grade implementation in C++:
#include <vector>
#include <algorithm>
#include <cmath>
class Solution {
private:
// Helper function to calculate total sum of divisions for a given divisor (d)
int calculateSum(const std::vector<int>& nums, int d) {
int sum = 0;
for (int num : nums) {
sum += std::ceil((double)(num) / (double)(d));
}
return sum;
}
public:
int smallestDivisor(std::vector<int>& nums, int threshold) {
int low = 1;
int high = *std::max_element(nums.begin(), nums.end());
int smallestValidDivisor = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (calculateSum(nums, mid) <= threshold) {
smallestValidDivisor = mid; // Record valid answer
high = mid - 1; // Try to find a smaller divisor on the left
} else {
low = mid + 1; // Sum too large, need a larger divisor on the right
}
}
return smallestValidDivisor;
}
};
8. Industry Reality, Edge Cases & Debugging
1. Integer Division Pitfall
- The Issue: In languages like C++, Java, or Python, dividing two integers (
a / b) performs integer truncation (rounding down), not ceiling division. - The Fix: Cast operands to
doubleorfloatbefore passing them toceil(), or use integer arithmetic tricks to simulate ceiling without floating-point inaccuracies:
// Integer division ceiling equivalent:
sum += (num + d - 1) / d;
2. Impossible Threshold Scenarios (Edge Case)
- What if even the maximum possible divisor (
Max(Array)) cannot bring the sum under the threshold? - Proof: The minimum possible sum you can achieve is when dividing every element by the maximum element of the array, which yields for each element, resulting in a total sum equal to the array size (
nums.size()). - If
Threshold < nums.size(), it is mathematically impossible to satisfy the condition, and the function should return-1.
9. Complexity Analysis of Binary Search Solution
- Time Complexity: , where is the size of the array and is the maximum element in
nums. The binary search runs in iterations, and inside each iteration, we traverse the array of size to calculate the division sum. - Space Complexity: auxiliary space, as no extra data structures are used.
10. Summary and Key Takeaways
- Problem Type: Finding the minimum value within a monotonic range perfectly maps to Binary Search on Answers.
- Search Space Bounds: The divisor search range always lies between
1and the maximum element in the array (Max). - Ceiling Division: Always account for fractional remainders by using ceiling operations (
ceil()) or integer ceiling arithmetic(num + d - 1) / d. - Optimization Impact: Upgraded time complexity from linear to logarithmic .
- Pointer Behavior: When searching for the minimum valid answer, store the valid mid and shrink the search space to the left (
high = mid - 1).