Taro Logo

Min Cost to Connect All Points

Medium
Google logo
Google
2 views
Topics:
GraphsGreedy Algorithms

You are given an array points representing integer coordinates of some points on a 2D-plane, where points[i] = [x_i, y_i]. The cost of connecting two points [x_i, y_i] and [x_j, y_j] is the manhattan distance between them: |x_i - x_j| + |y_i - y_j|, where |val| denotes the absolute value of val.

Return the minimum cost to make all points connected. All points are connected if there is exactly one simple path between any two points.

Example 1:

Input: points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
Output: 20
Explanation: We can connect the points as shown above to get the minimum cost of 20.
Notice that there is a unique path between every pair of points.

Example 2:

Input: points = [[3,12],[-2,5],[-4,1]]
Output: 18

Can you provide an algorithm to efficiently solve this problem? Consider edge cases and analyze time and space complexity.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What is the maximum number of points, and what is the range of possible x and y coordinates for each point?
  2. Can the input array of points be empty or contain only one point?
  3. Is it possible to have duplicate points (points with the same x and y coordinates)? If so, how should they be handled?
  4. Are the x and y coordinates integers, or could they be floating-point numbers?
  5. Is it guaranteed that a solution (a way to connect all points) always exists?

Brute Force Solution

Approach

The goal is to connect all points with the lowest possible total cost. The brute force approach considers every possible way to connect the points, calculating the cost of each way, and then finds the minimum cost among all of them.

Here's how the algorithm would work step-by-step:

  1. First, consider every possible pair of points and calculate the distance between them. These distances are the potential connection costs.
  2. Then, think about all possible ways to pick connections between the points so that every point is directly or indirectly connected to every other point. It's like trying every possible set of roads to connect all the cities.
  3. For each of these possible sets of connections, add up the costs of all the connections in that set. This gives you the total cost for that particular way of connecting all the points.
  4. After calculating the total cost for every possible way to connect all the points, find the smallest total cost among all of them. This smallest cost is the minimum cost to connect all the points.

Code Implementation

def min_cost_connect_all_points_brute_force(points):
    number_of_points = len(points)

    # Calculate the manhattan distance between all pairs of points
    all_edges = []
    for i in range(number_of_points):
        for j in range(i + 1, number_of_points):
            distance = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
            all_edges.append((i, j, distance))

    minimum_cost = float('inf')

    # Iterate through all possible subsets of edges
    for i in range(2 ** len(all_edges)):
        current_cost = 0
        selected_edges = []
        for j in range(len(all_edges)):
            if (i >> j) & 1:
                selected_edges.append(all_edges[j])
                current_cost += all_edges[j][2]

        # Check if the selected edges connect all points
        if is_connected(number_of_points, selected_edges):
            # Update minimum_cost if current_cost is smaller
            minimum_cost = min(minimum_cost, current_cost)

    return minimum_cost

def is_connected(number_of_points, edges):
    # Use Depth First Search to check connectivity
    if not edges:
        return number_of_points <= 1

    graph = {i: [] for i in range(number_of_points)}
    for start, end, _ in edges:
        graph[start].append(end)
        graph[end].append(start)

    visited = [False] * number_of_points
    
    #Start at node 0 and traverse
    dfs(graph, 0, visited)

    # Check if all nodes were visited
    return all(visited)

def dfs(graph, node, visited):
    visited[node] = True
    for neighbor in graph[node]:
        if not visited[neighbor]:
            dfs(graph, neighbor, visited)

Big(O) Analysis

Time Complexity
O(n^n)The brute force approach first calculates the Manhattan distance between all pairs of points, which takes O(n^2) time. Then, it considers all possible subsets of these edges to find a connected component that includes all points. Since there are approximately n choose 2 possible edges (n*(n-1)/2), the number of subsets is on the order of 2^(n*(n-1)/2). For each subset, we need to check if it connects all points, which may involve operations on the order of n. The dominant factor is the number of possible edge subsets, leading to a time complexity significantly higher than exponential, roughly O(n^n).
Space Complexity
O(N^2)The brute force approach implicitly calculates and stores the distances between all pairs of points. Given N points, there are N*(N-1)/2 possible pairs. Therefore, the space needed to store these distances scales quadratically with the number of points, resulting in O(N^2) space complexity. This represents the space needed to keep track of all potential connection costs. No additional significant auxiliary data structures are used other than what is needed to store the potential connections.

Optimal Solution

Approach

The goal is to connect all points with the lowest possible total cost. We use a method that gradually builds the network of connections, always choosing the cheapest connection that doesn't create a closed loop.

Here's how the algorithm would work step-by-step:

  1. Imagine each point as its own separate group.
  2. Calculate the cost (distance) between every pair of points.
  3. Sort these costs from smallest to largest.
  4. Start with the smallest cost. Check if the two points connected by this cost belong to different groups.
  5. If they do belong to different groups, connect them (add the cost to our total) and merge the two groups into one larger group.
  6. If they belong to the same group, skip this cost because connecting them would create a closed loop.
  7. Repeat steps 4-6 until all points belong to the same group (meaning everything is connected).
  8. The total of all the costs you added is the minimum cost to connect all points.

Code Implementation

def min_cost_connect_points(points):
    number_of_points = len(points)
    edges = []
    
    for i in range(number_of_points):
        for j in range(i + 1, number_of_points):
            distance = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
            edges.append((distance, i, j))
    
    edges.sort()
    
    parent = list(range(number_of_points))

    def find(point_index):
        if parent[point_index] != point_index:
            parent[point_index] = find(parent[point_index])
        return parent[point_index]

    def union(point_index_one, point_index_two):
        root_one = find(point_index_one)
        root_two = find(point_index_two)
        parent[root_one] = root_two

    minimum_cost = 0
    edges_used = 0

    for distance, point_index_one, point_index_two in edges:
        # Avoid cycles by checking if the points are already in the same group.
        if find(point_index_one) != find(point_index_two):
            minimum_cost += distance

            # Merge the two groups that the points belong to.
            union(point_index_one, point_index_two)

            edges_used += 1
            if edges_used == number_of_points - 1:
                break
    # Ensures all nodes are connected
    return minimum_cost

Big(O) Analysis

Time Complexity
O(n² log n)The algorithm's time complexity is dominated by two main operations: calculating the distances between all pairs of points and sorting those distances. Calculating the distance between all n points takes O(n²) time because we need to consider each pair of points. Sorting these n² distances takes O(n² log n²) which simplifies to O(n² log n). The union-find operations (checking group membership and merging groups) contribute a smaller factor and are generally considered close to constant time with path compression and union by rank, and do not impact the overall asymptotic complexity. Therefore, the overall time complexity is O(n² log n).
Space Complexity
O(N^2)The algorithm calculates the cost between every pair of points, potentially storing these costs in a list for sorting. Since there are N points, there are N*(N-1)/2 pairs, requiring space proportional to N^2. Additionally, the algorithm uses a data structure to track the group each point belongs to, typically a list or a dictionary of size N. However, the dominant space usage is for storing the costs between all pairs of points. Therefore, the auxiliary space complexity is O(N^2).

Edge Cases

CaseHow to Handle
Empty input arrayReturn 0 since no points need to be connected, implying zero cost.
Input array with only one pointReturn 0 since a single point is already considered connected to itself.
Large input array (n > 1000) approaching maximum constraintKruskal's or Prim's algorithm with efficient union-find or min-heap implementations, respectively, should be used to ensure O(n^2 log n) or O(n^2) time complexity, preventing timeout.
Points with identical coordinatesThe Manhattan distance will be zero, which should be correctly handled by the minimum spanning tree algorithm, and will be prioritized to include this edge early on.
Integer overflow when calculating Manhattan distance for extremely large coordinatesUse 64-bit integers (long long in C++, long in Java) to store the Manhattan distance to prevent overflow.
Negative coordinates in the input pointsThe Manhattan distance calculation |x1 - x2| + |y1 - y2| handles negative coordinates correctly due to the absolute value.
All points are located on the same line (collinear)Minimum spanning tree algorithms like Kruskal's or Prim's will still find the minimum cost to connect all points, forming a single path along the line.
Duplicate edges in the edge list for Kruskal's due to different points having the same Manhattan distanceThe sorting of edges by cost in Kruskal's algorithm will handle these duplicates correctly, and the union-find data structure will prevent cycles.