1515. Best Position for a Service Centre
Problem Description
A delivery company needs to find the optimal location for a new service center in a city. The company has the coordinates of all customers on a 2D map and wants to position the service center to minimize the total distance to all customers.
Given an array positions
where positions[i] = [xi, yi]
represents the coordinates of the i-th
customer, you need to find the location [x_centre, y_centre]
for the service center that minimizes the sum of Euclidean distances to all customers.
The Euclidean distance between two points (x1, y1)
and (x2, y2)
is calculated as √((x1-x2)² + (y1-y2)²)
.
Your goal is to minimize the total sum:
Σ √((x_centre - xi)² + (y_centre - yi)²)
for all customers i
.
The solution uses gradient descent optimization to find the optimal center position. Starting from the centroid (average position) of all customers, it iteratively adjusts the center location by following the negative gradient direction. The gradient indicates how the total distance changes with respect to small movements in x and y directions. The learning rate (alpha
) gradually decreases to ensure convergence, and the algorithm stops when the position changes become smaller than a threshold (eps
).
The answer should be accurate within 10^-5
of the actual minimum sum of distances.
Intuition
The key insight is that finding the optimal service center location is a continuous optimization problem where we want to minimize a smooth, convex function. Unlike finding the median (which minimizes Manhattan distance), minimizing Euclidean distances doesn't have a simple closed-form solution.
Think of this problem as finding the lowest point in a valley. If you're standing anywhere on the valley slope, the steepest downhill direction tells you which way to move to get lower. This is exactly what gradient descent does - it calculates the "slope" (gradient) at our current position and moves in the opposite direction to go downhill.
The gradient at any point tells us how the total distance changes when we make small movements. For each customer at position (xi, yi)
, their contribution to the gradient is proportional to the direction from our current center to that customer, weighted by 1/distance
. This makes intuitive sense: customers who are farther away have less "pull" on where the center should be.
Starting from the centroid (average of all customer positions) is a natural choice because it's already a reasonable approximation - it would be optimal if we were minimizing squared distances instead of regular distances.
The learning rate alpha
starts at 0.5 and gradually decreases (multiplied by 0.999 each iteration). This is crucial because:
- Initially, we want to take larger steps to quickly approach the optimal region
- As we get closer to the minimum, smaller steps prevent overshooting
- The decaying rate ensures convergence to a stable solution
The algorithm terminates when the position updates become negligibly small (less than eps = 1e-6
), indicating we've reached a local minimum which, due to the convex nature of the problem, is also the global minimum.
Learn more about Math patterns.
Solution Approach
The implementation uses gradient descent optimization to find the optimal service center location. Here's a detailed walkthrough of the algorithm:
Initialization:
- Calculate the centroid of all customer positions as the starting point:
x = Σxi/n
,y = Σyi/n
- Set the initial learning rate
alpha = 0.5
- Define the decay factor
decay = 0.999
to gradually reduce the learning rate - Set convergence threshold
eps = 1e-6
Gradient Descent Loop:
-
Calculate Gradient and Total Distance: For each iteration, compute the gradient components and current total distance:
grad_x = grad_y = 0 dist = 0 for x1, y1 in positions: a = x - x1 # x-component of vector from customer to center b = y - y1 # y-component of vector from customer to center c = sqrt(a * a + b * b) # Euclidean distance grad_x += a / (c + 1e-8) # Partial derivative w.r.t x grad_y += b / (c + 1e-8) # Partial derivative w.r.t y dist += c # Accumulate total distance
The gradient formula comes from the derivative of
√((x-xi)² + (y-yi)²)
:∂/∂x = (x-xi)/√((x-xi)² + (y-yi)²)
∂/∂y = (y-yi)/√((x-xi)² + (y-yi)²)
The
1e-8
term prevents division by zero when the center coincides with a customer location. -
Update Position: Move the center in the negative gradient direction (downhill):
dx = grad_x * alpha dy = grad_y * alpha x -= dx y -= dy
-
Decay Learning Rate:
alpha *= decay
This ensures smaller steps as we approach the minimum, improving convergence stability.
-
Check Convergence:
if abs(dx) <= eps and abs(dy) <= eps: return dist
When position updates become smaller than the threshold, we've found the optimal location and return the minimum sum of distances.
The algorithm leverages the convexity of the objective function - the sum of Euclidean distances forms a convex surface with a single global minimum, guaranteeing that gradient descent will find the optimal solution regardless of minor variations in the starting point.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 3 customers to illustrate how gradient descent finds the optimal service center location.
Given: positions = [[0, 1], [1, 0], [1, 2]]
Step 1: Initialize at Centroid
- Calculate starting position (centroid):
- x = (0 + 1 + 1) / 3 = 0.667
- y = (1 + 0 + 2) / 3 = 1.0
- Starting point:
(0.667, 1.0)
- Initial learning rate:
alpha = 0.5
Step 2: First Iteration - Calculate Gradient
For each customer, calculate their contribution to the gradient:
Customer 1 at (0, 1)
:
- Vector from customer to center:
a = 0.667 - 0 = 0.667
,b = 1.0 - 1 = 0
- Distance:
c = √(0.667² + 0²) = 0.667
- Gradient contribution:
grad_x += 0.667/0.667 = 1.0
,grad_y += 0/0.667 = 0
Customer 2 at (1, 0)
:
- Vector:
a = 0.667 - 1 = -0.333
,b = 1.0 - 0 = 1.0
- Distance:
c = √(0.333² + 1.0²) = 1.054
- Gradient contribution:
grad_x += -0.333/1.054 = -0.316
,grad_y += 1.0/1.054 = 0.949
Customer 3 at (1, 2)
:
- Vector:
a = 0.667 - 1 = -0.333
,b = 1.0 - 2 = -1.0
- Distance:
c = √(0.333² + 1.0²) = 1.054
- Gradient contribution:
grad_x += -0.333/1.054 = -0.316
,grad_y += -1.0/1.054 = -0.949
Total gradient: grad_x = 1.0 - 0.316 - 0.316 = 0.368
, grad_y = 0 + 0.949 - 0.949 = 0
Total distance at this position: 0.667 + 1.054 + 1.054 = 2.775
Step 3: Update Position
- Movement:
dx = 0.368 * 0.5 = 0.184
,dy = 0 * 0.5 = 0
- New position:
x = 0.667 - 0.184 = 0.483
,y = 1.0 - 0 = 1.0
- Update learning rate:
alpha = 0.5 * 0.999 = 0.4995
Step 4: Check Convergence
- Since
|dx| = 0.184 > 1e-6
, continue iterating
Subsequent Iterations:
The algorithm continues this process:
- Iteration 2: Position moves to approximately
(0.395, 1.0)
, distance = 2.733 - Iteration 3: Position moves to approximately
(0.350, 1.0)
, distance = 2.718 - ...
- After many iterations: Position converges to approximately
(0.5, 1.0)
Final Result:
The optimal service center location is near (0.5, 1.0)
with a minimum total distance of approximately 2.707.
This makes intuitive sense - the optimal location is pulled towards the cluster of two customers at x=1 (customers 2 and 3) but balanced by customer 1 at x=0. The y-coordinate settles at 1.0, which is the median of the y-values (0, 1, 2), minimizing vertical distances.
The gradient descent successfully navigates from the initial centroid to the true geometric median, demonstrating how the algorithm efficiently finds the point that minimizes the sum of Euclidean distances to all customers.
Solution Implementation
1from typing import List
2from math import sqrt
3
4
5class Solution:
6 def getMinDistSum(self, positions: List[List[int]]) -> float:
7 """
8 Find the geometric median - a point that minimizes the sum of Euclidean distances
9 to all given positions using gradient descent optimization.
10
11 Args:
12 positions: List of [x, y] coordinates
13
14 Returns:
15 Minimum sum of distances from the optimal point to all positions
16 """
17 # Calculate the centroid as initial guess for the optimal point
18 num_positions = len(positions)
19 center_x = sum(pos[0] for pos in positions) / num_positions
20 center_y = sum(pos[1] for pos in positions) / num_positions
21
22 # Gradient descent parameters
23 learning_rate = 0.5 # Initial step size for gradient descent
24 decay_factor = 0.999 # Decay factor to reduce learning rate over iterations
25 convergence_threshold = 1e-6 # Threshold for convergence detection
26 epsilon = 1e-8 # Small value to avoid division by zero
27
28 # Iterative gradient descent optimization
29 while True:
30 # Calculate gradients and total distance
31 gradient_x = 0.0
32 gradient_y = 0.0
33 total_distance = 0.0
34
35 for position_x, position_y in positions:
36 # Calculate distance components
37 delta_x = center_x - position_x
38 delta_y = center_y - position_y
39 distance = sqrt(delta_x * delta_x + delta_y * delta_y)
40
41 # Accumulate partial derivatives (gradient components)
42 # Adding epsilon to avoid division by zero when point coincides with a position
43 gradient_x += delta_x / (distance + epsilon)
44 gradient_y += delta_y / (distance + epsilon)
45
46 # Accumulate total distance
47 total_distance += distance
48
49 # Calculate step sizes based on gradients and learning rate
50 step_x = gradient_x * learning_rate
51 step_y = gradient_y * learning_rate
52
53 # Update center point by moving against gradient direction
54 center_x -= step_x
55 center_y -= step_y
56
57 # Decay the learning rate for finer adjustments
58 learning_rate *= decay_factor
59
60 # Check convergence: if steps are smaller than threshold, we've converged
61 if abs(step_x) <= convergence_threshold and abs(step_y) <= convergence_threshold:
62 return total_distance
63
1class Solution {
2 /**
3 * Finds the geometric median (Fermat point) that minimizes the sum of distances
4 * to all given positions using gradient descent optimization.
5 *
6 * @param positions 2D array where each element represents a point [x, y]
7 * @return minimum sum of distances from the optimal point to all positions
8 */
9 public double getMinDistSum(int[][] positions) {
10 int numPoints = positions.length;
11
12 // Initialize starting point as the centroid (average of all positions)
13 double currentX = 0;
14 double currentY = 0;
15 for (int[] position : positions) {
16 currentX += position[0];
17 currentY += position[1];
18 }
19 currentX /= numPoints;
20 currentY /= numPoints;
21
22 // Gradient descent parameters
23 double decayRate = 0.999; // Learning rate decay factor
24 double convergenceThreshold = 1e-6; // Convergence criterion
25 double learningRate = 0.5; // Initial step size for gradient descent
26
27 // Gradient descent optimization loop
28 while (true) {
29 double gradientX = 0;
30 double gradientY = 0;
31 double totalDistance = 0;
32
33 // Calculate gradient and total distance
34 for (int[] position : positions) {
35 double deltaX = currentX - position[0];
36 double deltaY = currentY - position[1];
37 double distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
38
39 // Add small epsilon to avoid division by zero
40 gradientX += deltaX / (distance + 1e-8);
41 gradientY += deltaY / (distance + 1e-8);
42 totalDistance += distance;
43 }
44
45 // Calculate step size based on gradient and learning rate
46 double stepX = gradientX * learningRate;
47 double stepY = gradientY * learningRate;
48
49 // Check convergence: if steps are small enough, return result
50 if (Math.abs(stepX) <= convergenceThreshold &&
51 Math.abs(stepY) <= convergenceThreshold) {
52 return totalDistance;
53 }
54
55 // Update position by moving against the gradient
56 currentX -= stepX;
57 currentY -= stepY;
58
59 // Decay learning rate for finer adjustments
60 learningRate *= decayRate;
61 }
62 }
63}
64
1class Solution {
2public:
3 double getMinDistSum(vector<vector<int>>& positions) {
4 int numPositions = positions.size();
5
6 // Initialize the starting point as the centroid of all positions
7 double currentX = 0.0, currentY = 0.0;
8 for (const auto& position : positions) {
9 currentX += position[0];
10 currentY += position[1];
11 }
12 currentX /= numPositions;
13 currentY /= numPositions;
14
15 // Gradient descent parameters
16 double decayRate = 0.999; // Learning rate decay factor
17 double convergenceThreshold = 1e-6; // Convergence criterion
18 double learningRate = 0.5; // Initial learning rate
19
20 // Gradient descent optimization loop
21 while (true) {
22 double gradientX = 0.0, gradientY = 0.0;
23 double totalDistance = 0.0;
24
25 // Calculate gradient and total distance for current position
26 for (const auto& position : positions) {
27 double deltaX = currentX - position[0];
28 double deltaY = currentY - position[1];
29 double distance = sqrt(deltaX * deltaX + deltaY * deltaY);
30
31 // Add small epsilon to avoid division by zero
32 gradientX += deltaX / (distance + 1e-8);
33 gradientY += deltaY / (distance + 1e-8);
34 totalDistance += distance;
35 }
36
37 // Calculate the step size for this iteration
38 double stepX = gradientX * learningRate;
39 double stepY = gradientY * learningRate;
40
41 // Check convergence: if step size is smaller than threshold, stop
42 if (abs(stepX) <= convergenceThreshold && abs(stepY) <= convergenceThreshold) {
43 return totalDistance;
44 }
45
46 // Update position using gradient descent
47 currentX -= stepX;
48 currentY -= stepY;
49
50 // Decay the learning rate for next iteration
51 learningRate *= decayRate;
52 }
53 }
54};
55
1/**
2 * Finds the optimal meeting point that minimizes the sum of Euclidean distances
3 * to all given positions using gradient descent optimization.
4 *
5 * @param positions - Array of [x, y] coordinates representing positions
6 * @returns The minimum sum of distances from the optimal point to all positions
7 */
8function getMinDistSum(positions: number[][]): number {
9 const positionCount = positions.length;
10
11 // Initialize the starting point as the centroid (average position)
12 let currentX = 0;
13 let currentY = 0;
14
15 // Calculate the centroid coordinates
16 for (const [positionX, positionY] of positions) {
17 currentX += positionX;
18 currentY += positionY;
19 }
20 currentX /= positionCount;
21 currentY /= positionCount;
22
23 // Gradient descent parameters
24 const DECAY_RATE = 0.999; // Learning rate decay factor
25 const CONVERGENCE_THRESHOLD = 1e-6; // Convergence criterion for stopping
26 let learningRate = 0.5; // Initial learning rate for gradient descent
27
28 // Gradient descent optimization loop
29 while (true) {
30 // Initialize gradient components and total distance
31 let gradientX = 0;
32 let gradientY = 0;
33 let totalDistance = 0;
34
35 // Calculate gradient and total distance for current position
36 for (const [positionX, positionY] of positions) {
37 const deltaX = currentX - positionX;
38 const deltaY = currentY - positionY;
39 const euclideanDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
40
41 // Add partial derivatives to gradient (with small epsilon to avoid division by zero)
42 gradientX += deltaX / (euclideanDistance + 1e-8);
43 gradientY += deltaY / (euclideanDistance + 1e-8);
44
45 // Accumulate total distance
46 totalDistance += euclideanDistance;
47 }
48
49 // Calculate the step size for this iteration
50 const stepX = gradientX * learningRate;
51 const stepY = gradientY * learningRate;
52
53 // Check convergence: if step size is small enough, return the result
54 if (Math.abs(stepX) <= CONVERGENCE_THRESHOLD && Math.abs(stepY) <= CONVERGENCE_THRESHOLD) {
55 return totalDistance;
56 }
57
58 // Update position by moving in the opposite direction of gradient
59 currentX -= stepX;
60 currentY -= stepY;
61
62 // Decay the learning rate for next iteration
63 learningRate *= DECAY_RATE;
64 }
65}
66
Time and Space Complexity
Time Complexity: O(n * k)
where n
is the number of positions and k
is the number of iterations until convergence.
- The initial calculation of the centroid takes
O(n)
time to sum all coordinates. - The main gradient descent loop runs for
k
iterations until convergence (whenabs(dx) <= eps and abs(dy) <= eps
). - In each iteration, we iterate through all
n
positions to calculate:- The gradient components
grad_x
andgrad_y
- The total distance
dist
- This inner loop takes
O(n)
time per iteration
- The gradient components
- The number of iterations
k
depends on the convergence criteria and decay rate. Withdecay = 0.999
andeps = 1e-6
, the algorithm typically converges in a constant number of iterations that doesn't depend onn
, but in worst case could beO(log(1/eps))
. - Overall time complexity:
O(n * k)
wherek = O(log(1/eps))
in practice.
Space Complexity: O(1)
- The algorithm uses only a constant amount of extra space:
- Variables for coordinates:
x
,y
- Variables for gradients:
grad_x
,grad_y
- Variables for updates:
dx
,dy
- Other scalar variables:
dist
,alpha
,decay
,eps
,n
,a
,b
,c
- Variables for coordinates:
- All these variables require constant space regardless of the input size.
- The input list
positions
is not counted as extra space since it's part of the input.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Numerical Instability When Distance Approaches Zero
Problem: When the center point gets very close to or coincides with a customer position, the distance becomes zero or near-zero, causing division by zero in the gradient calculation. This leads to NaN
or infinite values that corrupt the optimization process.
Example Scenario:
# If center_x = 5, center_y = 3 and a customer is at [5, 3] delta_x = 5 - 5 = 0 delta_y = 3 - 3 = 0 distance = sqrt(0 + 0) = 0 gradient_x += 0 / 0 # Division by zero!
Solution: Add a small epsilon value to the denominator:
gradient_x += delta_x / (distance + epsilon) # epsilon = 1e-8 gradient_y += delta_y / (distance + epsilon)
2. Premature Convergence with Fixed Learning Rate
Problem: Using a constant learning rate can cause the algorithm to oscillate around the minimum without converging, or converge too slowly. Large learning rates cause overshooting, while small rates make convergence extremely slow.
Example of Oscillation:
# With fixed learning_rate = 1.0 # The center might jump back and forth across the optimal point # Iteration 1: center_x = 10 → 2 (overshoot) # Iteration 2: center_x = 2 → 9 (overshoot back) # ... continues oscillating
Solution: Implement learning rate decay:
learning_rate *= decay_factor # Gradually reduce step size # Start with learning_rate = 0.5 # After 100 iterations: 0.5 * (0.999^100) ≈ 0.45 # After 1000 iterations: 0.5 * (0.999^1000) ≈ 0.18
3. Poor Initial Point Selection
Problem: Starting from an arbitrary point (like origin [0,0]) when all customers are far from it can lead to slow convergence and numerical issues due to large gradients.
Example:
# If all customers are around [1000, 1000] but we start at [0, 0] # Initial gradients will be very large, causing unstable updates
Solution: Use the centroid (mean position) as the starting point:
center_x = sum(pos[0] for pos in positions) / len(positions)
center_y = sum(pos[1] for pos in positions) / len(positions)
4. Incorrect Convergence Criteria
Problem: Using absolute distance threshold instead of step size can cause premature termination when far from optimum, or infinite loops when the threshold is too strict.
Incorrect approach:
# Wrong: checking if total distance change is small
if abs(current_dist - previous_dist) < threshold:
return # Might stop when on a plateau, not at minimum
Solution: Check if the position updates (step sizes) are small:
if abs(step_x) <= convergence_threshold and abs(step_y) <= convergence_threshold:
return total_distance # Converged when movement is negligible
5. Accumulating Floating-Point Errors
Problem: In problems with many customers or requiring high precision, small floating-point errors can accumulate and affect the final result accuracy.
Solution: Use appropriate data types and consider numerical stability:
# Use double precision floats gradient_x = 0.0 # Not 0 (which might be int) gradient_y = 0.0 # Consider using higher precision libraries for extreme cases # from decimal import Decimal (if needed)
In a binary min heap, the minimum element can be found in:
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!