Topics › Binary Search › BS on Answers
Minimize Max Distance to Gas Station
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Gas stations sit on a straight highway at the sorted, distinct positions stations[0] < stations[1] < ... < stations[n-1]. You may build k new stations anywhere on the line, including at non-integer positions. After building, look at the largest distance between two neighbouring stations; call it the penalty. Place the k new stations so that the penalty is as small as possible, and return that minimum penalty. Answers within 10^-6 of the true value are accepted.
Constraints: 1 ≤ n ≤ 2000, 0 ≤ stations[i] ≤ 10^8, 0 ≤ k ≤ 10^6.
Example 1
stations = [1, 2, 3, 4, 5], k = 4
answer = 0.5 // one new station in the middle of each of the four gaps
Example 2
stations = [1, 13, 17, 23], k = 5
answer = 3.0 // gaps 12, 4, 6 get 3, 1, 1 new stations: sections of 3, 2, 3
Example 3
stations = [1, 2, 3, 4, 5], k = 0
answer = 1.0 // nothing to build; the largest existing gap stays
Two observations drive everything below. First, a new station only matters inside the gap it lands in, so the problem is really about deciding how many new stations each gap receives. Second, inside one gap it is always best to spread its new stations evenly: a gap of length g that receives c stations is cut into c + 1 sections, the longest of which is at least g / (c + 1), and equal spacing achieves exactly that.
Approach 1: Brute force, greedy placement one station at a time
Intuition. Place the k stations one by one. Each time, look at every gap, compute the length of its sections given how many stations it already holds, and drop the next station into the gap whose sections are currently the longest. The penalty is the longest section anywhere, so the only way to reduce it is to shrink the current worst gap; putting a station anywhere else leaves the penalty unchanged. Repeating the locally forced move k times yields the optimum.
Algorithm.
- If there are fewer than two stations, there is no gap: return
0. - Keep
placed[i], the number of new stations already in gapi(betweenstations[i]andstations[i+1]). - Repeat
ktimes: find the gap maximising(stations[i+1] − stations[i]) / (placed[i] + 1)and increment itsplaced[i]. - Return the maximum section length over all gaps.
static double bruteForce(int[] stations, int k) {
int n = stations.length;
if (n < 2) return 0;
int[] placed = new int[n - 1]; // new stations already dropped into gap i
for (int t = 0; t < k; t++) {
int best = 0;
double bestLen = -1;
for (int i = 0; i < n - 1; i++) {
double len = (double) (stations[i + 1] - stations[i]) / (placed[i] + 1);
if (len > bestLen) { bestLen = len; best = i; }
}
placed[best]++;
}
double ans = 0;
for (int i = 0; i < n - 1; i++) {
ans = Math.max(ans, (double) (stations[i + 1] - stations[i]) / (placed[i] + 1));
}
return ans;
}
Complexity. Time O(k × n): each of the k placements scans all n − 1 gaps. Space O(n) for the counters.
With k up to 10^6 and n up to 2000 that is around 2 × 10^9 operations, too slow. The scan for the worst gap is the bottleneck.
Approach 2: Better, the same greedy with a max-heap
Intuition. The brute force spends O(n) per step just to find the gap with the longest sections. A priority queue keyed on section length hands over that gap in O(log n). Pop the worst gap, give it one more station, push it back with its new, shorter section length.
Algorithm.
- If
n < 2, return0. - Push every gap into a max-heap as
{sectionLength, gapIndex}; initially the section length is the full gap. - Repeat
ktimes: pop the top, incrementplaced[gap], push{gap / (placed[gap] + 1), gap}. - The top of the heap is now the longest section: return it.
static double better(int[] stations, int k) {
int n = stations.length;
if (n < 2) return 0;
int[] placed = new int[n - 1];
PriorityQueue<double[]> heap = new PriorityQueue<>((a, b) -> Double.compare(b[0], a[0])); // {sectionLength, gapIndex}
for (int i = 0; i < n - 1; i++) heap.offer(new double[]{stations[i + 1] - stations[i], i});
for (int t = 0; t < k; t++) {
int gap = (int) heap.poll()[1];
placed[gap]++;
heap.offer(new double[]{(double) (stations[gap + 1] - stations[gap]) / (placed[gap] + 1), gap});
}
return heap.peek()[0];
}
Complexity. Time O((n + k) log n): building the heap and k pop/push rounds. Space O(n).
This is a real improvement, but the work still grows with k. Building a million stations one at a time is wasteful when we could ask a different question entirely.
Approach 3: Optimal, binary search on the answer
Intuition. Flip the question around. Instead of “where do the stations go?”, ask “can we make every section at most d long with at most k new stations?” For a single gap of length g, the fewest stations that achieve sections of length at most d is ceil(g / d) − 1: the gap must be cut into at least ceil(g / d) sections, and s sections need s − 1 cuts. Summing over all gaps gives the minimum number of stations needed for the penalty d; compare it to k.
That predicate is monotone in d: a larger allowed distance never needs more stations. The answer is the smallest d for which the predicate holds, which binary search finds. Because d is a real number, we do not stop when lo > hi; we halve the interval a fixed number of times, which shrinks it far below the required precision.
Algorithm.
- If
n < 2, return0. - Set
lo = 0andhito the largest existing gap (placing nothing already achieves that penalty). - Repeat 100 times:
mid = (lo + hi) / 2. Iffeasible(mid), sethi = mid; otherwise setlo = mid. - Return
hi, which is always a feasible value.
static boolean feasible(int[] stations, int k, double d) {
long needed = 0;
for (int i = 0; i < stations.length - 1; i++) {
int gap = stations[i + 1] - stations[i];
long inside = (long) Math.ceil(gap / d) - 1; // ceil(gap / d) sections need one fewer new stations
if (inside > k - needed) return false;
needed += inside;
}
return true;
}
static double optimal(int[] stations, int k) {
int n = stations.length;
if (n < 2) return 0;
double lo = 0, hi = 0;
for (int i = 0; i < n - 1; i++) hi = Math.max(hi, stations[i + 1] - stations[i]);
for (int iter = 0; iter < 100; iter++) { // 100 halvings shrink any gap far below 1e-6
double mid = (lo + hi) / 2;
if (feasible(stations, k, mid)) hi = mid; else lo = mid;
}
return hi;
}
Dry run of the feasibility check on stations = [1, 13, 17, 23], k = 5. The gaps are 12, 4, 6.
| d | stations for gap 12 | for gap 4 | for gap 6 | total | ≤ 5? |
|---|---|---|---|---|---|
| 6.0 | ceil(2) − 1 = 1 | ceil(0.67) − 1 = 0 | ceil(1) − 1 = 0 | 1 | yes |
| 3.0 | ceil(4) − 1 = 3 | ceil(1.33) − 1 = 1 | ceil(2) − 1 = 1 | 5 | yes |
| 2.9 | ceil(4.14) − 1 = 4 | ceil(1.38) − 1 = 1 | ceil(2.07) − 1 = 2 | 7 | no |
Every d at or above 3 is feasible and every d below 3 is not, so the halving converges to 3.0.
Complexity. Time O(n × I) where I = 100 is the number of halvings, independent of k. Space O(1).
Why 100 iterations? Each halving cuts the interval in two, so after 100 steps an initial width of 10^8 has shrunk by a factor of 2^100, far beyond the 10^-6 tolerance and beyond what a double can even represent. An alternative stopping rule is while (hi − lo > 1e-6); the fixed count is simpler to reason about and immune to floating-point stalls.
Edge cases and pitfalls
- Fewer than two stations. There is no gap, so the penalty is
0. All three methods return early; without the guard the brute force would indexplaced[0]on a zero-length array. k = 0. Nothing is built and the answer is the largest existing gap, which is exactly the initialhi; every probe below it is infeasible.- Integer division.
gap / dmust be floating-point division. The code divides anintby adouble, which promotes theint; dividing twoints would truncate and under-count sections. - Precision of
ceil.Math.ceil(gap / d)can land on an exact integer, as with12 / 3.0 = 4.0; that is correct, because four sections of exactly3need three stations. Values ofda hair below the answer give one extra section and fail the check, which is what makes the search converge to the boundary from above. - Return
hi, notloormid.hiis always a feasible penalty,lois always infeasible (or the initial0). Returninghiguarantees the result is achievable. - Counting with
long. Withkup to10^6anintwould suffice, butneededandinsidearelongso that a tinydproducing hugeceilvalues cannot overflow before the earlyreturn false. - Greedy tie-breaking. The brute force picks the first gap among equals. Any tie-breaking works, since the penalty only depends on how many stations each gap gets, not on the order in which they are placed.
Related problems
- Aggressive Cows also searches for a distance, but maximises it; compare how the feasibility direction flips.
- Koko eating bananas uses the same “sum of ceilings under a budget” check with integers.
- Find Nth root of a number and Find square root of a number introduce searching over a numeric range rather than over indices.
- Minimum days to make M bouquets is another minimum-feasible-value search with a linear check.