Taro Logo

Power Grid Maintenance

Medium
Asked by:
Profile picture
13 views
Topics:
GraphsArraysDynamic Programming

You are given an integer c representing c power stations, each with a unique identifier id from 1 to c (1‑based indexing).

These stations are interconnected via n bidirectional cables, represented by a 2D array connections, where each element connections[i] = [ui, vi] indicates a connection between station ui and station vi. Stations that are directly or indirectly connected form a power grid.

Initially, all stations are online (operational).

You are also given a 2D array queries, where each query is one of the following two types:

  • [1, x]: A maintenance check is requested for station x. If station x is online, it resolves the check by itself. If station x is offline, the check is resolved by the operational station with the smallest id in the same power grid as x. If no operational station exists in that grid, return -1.

  • [2, x]: Station x goes offline (i.e., it becomes non-operational).

Return an array of integers representing the results of each query of type [1, x] in the order they appear.

Note: The power grid preserves its structure; an offline (non‑operational) node remains part of its grid and taking it offline does not alter connectivity.

Example 1:

Input: c = 5, connections = [[1,2],[2,3],[3,4],[4,5]], queries = [[1,3],[2,1],[1,1],[2,2],[1,2]]

Output: [3,2,3]

Explanation:

  • Initially, all stations {1, 2, 3, 4, 5} are online and form a single power grid.
  • Query [1,3]: Station 3 is online, so the maintenance check is resolved by station 3.
  • Query [2,1]: Station 1 goes offline. The remaining online stations are {2, 3, 4, 5}.
  • Query [1,1]: Station 1 is offline, so the check is resolved by the operational station with the smallest id among {2, 3, 4, 5}, which is station 2.
  • Query [2,2]: Station 2 goes offline. The remaining online stations are {3, 4, 5}.
  • Query [1,2]: Station 2 is offline, so the check is resolved by the operational station with the smallest id among {3, 4, 5}, which is station 3.

Example 2:

Input: c = 3, connections = [], queries = [[1,1],[2,1],[1,1]]

Output: [1,-1]

Explanation:

  • There are no connections, so each station is its own isolated grid.
  • Query [1,1]: Station 1 is online in its isolated grid, so the maintenance check is resolved by station 1.
  • Query [2,1]: Station 1 goes offline.
  • Query [1,1]: Station 1 is offline and there are no other stations in its grid, so the result is -1.

Constraints:

  • 1 <= c <= 105
  • 0 <= n == connections.length <= min(105, c * (c - 1) / 2)
  • connections[i].length == 2
  • 1 <= ui, vi <= c
  • ui != vi
  • 1 <= queries.length <= 2 * 105
  • queries[i].length == 2
  • queries[i][0] is either 1 or 2.
  • 1 <= queries[i][1] <= c

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. Can the `connections` array contain duplicate edges, for instance, `[1, 2]` appearing multiple times, or both `[1, 2]` and `[2, 1]` being present?
  2. If a 'go offline' query of type `[2, x]` is issued for a station `x` that is already offline, should I simply treat this as a no-op?
  3. To confirm my understanding of a key edge case: if all stations in a particular power grid go offline, any subsequent type 1 query for a station in that grid should return -1, correct?
  4. The problem structure implies that queries must be processed sequentially as the state of stations changes. Am I right to assume this is an 'online' problem, where I cannot reorder or look ahead at the full list of queries?
  5. For a type 1 query on an offline station, finding the smallest-ID online station in its grid seems crucial. Since this minimum can change as stations go offline, is designing an efficient way to find this new minimum a key part of the problem?

Brute Force Solution

Approach

The brute force strategy is to exhaustively check every single possible maintenance plan. We will identify all the non-essential power lines, consider every possible combination of them to disconnect, and for each combination, we'll count how many separate power grids are created.

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

  1. First, identify all the power lines that are not critical and can be taken offline for maintenance.
  2. Next, create every single possible group of these non-critical lines that you could disconnect, where each group has the required number of lines for the maintenance task.
  3. Take the first group from your list of possibilities. Imagine for a moment that these specific power lines are removed from the grid.
  4. With these lines gone, trace the connections from each city to see how many separate, isolated networks remain.
  5. Count the total number of these separate networks and keep a record of it.
  6. Now, put those lines back and repeat the process for the next group of lines you could disconnect. Do this for every single possible group you identified earlier.
  7. Once you have checked every combination and have a count of separate networks for each one, find the smallest count. This is the minimum number of separate grids possible, which is your answer.

Code Implementation

def find_minimum_cost_brute_force(number_of_cities, available_power_lines):
    minimum_total_cost = 999999999

    # To guarantee finding the optimal grid, we must test every possible combination of power lines.

    for subset_size in range(1, len(available_power_lines) + 1):
        # Note: This code assumes 'itertools.combinations' is available.
        for power_line_subset in itertools.combinations(available_power_lines, subset_size):

            # To be a valid solution, a set of lines must connect all cities into one network.

            parent_map = list(range(number_of_cities))
            number_of_components = number_of_cities

            def find_root(city_index):
                if parent_map[city_index] == city_index:
                    return city_index
                parent_map[city_index] = find_root(parent_map[city_index])
                return parent_map[city_index]

            def union_components(city_one, city_two):
                nonlocal number_of_components
                root_one = find_root(city_one)
                root_two = find_root(city_two)
                if root_one != root_two:
                    parent_map[root_two] = root_one
                    number_of_components -= 1

            for line_city_one, line_city_two, _ in power_line_subset:
                union_components(line_city_one, line_city_two)

            # We only calculate the cost for valid grids that link every city into a single component.

            if number_of_components == 1:
                current_subset_cost = sum(cost for _, _, cost in power_line_subset)

                # The total cost is updated if this valid grid is cheaper than the best one found so far.

                minimum_total_cost = min(minimum_total_cost, current_subset_cost)

    if minimum_total_cost == 999999999:
        return -1
    else:
        return minimum_total_cost

Big(O) Analysis

Time Complexity
O(C(n, k) * (V + E))The primary cost is driven by the need to check every combination of k power lines chosen from the n non-critical ones available. The number of these combinations is C(n, k), which grows exponentially with n. For each individual combination, a full traversal of the graph with V cities and E power lines is required to count the separate networks, taking O(V + E) time. Therefore, the total operations approximate the number of combinations multiplied by the cost to analyze the grid, simplifying to O(C(n, k) * (V + E)).
Space Complexity
O(k * C(M, k))The primary driver of space complexity is the explicit creation and storage of 'every single possible group' of non-critical lines. If there are M non-critical lines and k are chosen for maintenance, this requires storing C(M, k) combinations, each of size k, resulting in O(k * C(M, k)) space. Additionally, for each combination, tracing connections requires a visited data structure of size N, where N is the number of cities. However, the memory used to store the vast number of combinations is the dominant factor, making the overall auxiliary space complexity exponential.

Optimal Solution

Approach

Instead of trying to figure out the entire grid layout at once, the best approach is to build it one piece at a time. We can do this by repeatedly adding the cheapest possible power line that doesn't create a redundant connection, guaranteeing the lowest total cost in the end.

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

  1. First, make a list of every single possible power line that could be built between any two cities, and note the cost for each one.
  2. Sort this entire list of potential power lines, arranging them from the least expensive to the most expensive.
  3. Now, start going through your sorted list, beginning with the absolute cheapest power line available.
  4. For each potential line, check if the two cities it would join are already connected to each other, even indirectly, through the lines you've already decided to build.
  5. If they are already connected, this new line is redundant and would create a wasteful loop. You must ignore it and move on to the next cheapest option.
  6. If the two cities are not yet connected, then this line is a valuable link. Add it to your final grid plan. This action effectively merges two previously separate network sections into one.
  7. Continue this process of checking and adding the next-cheapest useful line until every city in the grid is connected to every other city.
  8. Once all cities form a single connected network, you have found the set of lines that connects everything for the minimum possible cost.

Code Implementation

def power_grid_maintenance(number_of_cities, all_potential_lines):
    parent_of_city = list(range(number_of_cities))

    def find_island_representative(city_index):
        if parent_of_city[city_index] == city_index:
            return city_index
        # Path compression makes subsequent lookups for this city and its children much faster.
        parent_of_city[city_index] = find_island_representative(parent_of_city[city_index])
        return parent_of_city[city_index]

    # Sort all potential connections by cost to greedily evaluate the cheapest options first.
    all_potential_lines.sort(key=lambda line_details: line_details[2])

    total_maintenance_cost = 0
    number_of_lines_built = 0

    for city_one, city_two, connection_cost in all_potential_lines:
        root_for_city_one = find_island_representative(city_one)
        root_for_city_two = find_island_representative(city_two)

        # A connection is only useful if it joins two previously disconnected grid sections.
        if root_for_city_one != root_for_city_two:
            total_maintenance_cost += connection_cost
            
            # Merge the two separate islands, effectively building the new power line.
            parent_of_city[root_for_city_two] = root_for_city_one
            
            number_of_lines_built += 1
            if number_of_lines_built == number_of_cities - 1:
                break
    
    return total_maintenance_cost

Big(O) Analysis

Time Complexity
O(n² log n)The dominant factor in the runtime is the requirement to handle all possible connections between the n cities. The number of these potential connections, or edges (E), is proportional to n squared. The most computationally expensive step in the described approach is sorting this complete list of E edges by cost, which has a time complexity of O(E log E). By substituting n² for E, the complexity becomes O(n² log(n²)), which simplifies to O(n² log n). This sorting operation significantly outweighs the cost of iterating through the edges to build the final grid.
Space Complexity
O(N^2)The dominant space cost is for the list of all possible power lines, as described in the first step where N is the number of cities. For N cities, the number of potential connections is proportional to N^2, leading to a list that requires O(N^2) auxiliary memory. To track which cities are already connected, the algorithm implicitly uses a Disjoint Set Union data structure, which needs O(N) space for its internal arrays. Since the O(N^2) space for the power line list is the largest component, the overall auxiliary space complexity is O(N^2).

Edge Cases

The `connections` array is empty, resulting in `c` separate power grids, each with a single station.
How to Handle:
The underlying graph data structure, such as a Disjoint Set Union, correctly initializes each station as an independent component.
All stations in a specific power grid are turned offline before a type 1 query is made for a station in that grid.
How to Handle:
The solution must return -1 for such a query, which is managed by tracking the smallest operational station per grid and using a sentinel value for depleted grids.
A station is taken offline multiple times via repeated type 2 queries.
How to Handle:
The station's state is idempotent, so the solution correctly handles this by simply maintaining its offline status without error.
The `queries` array contains only type 1 (maintenance check) queries and no type 2 queries.
How to Handle:
In this case, all stations remain online, and for any query `[1, x]`, the solution will correctly return `x`.
All `c` stations are connected and form a single, large power grid.
How to Handle:
The solution's graph representation will group all stations under one root, and all queries will be resolved within this single component.
The system consists of only one station (`c=1`) with no connections.
How to Handle:
The solution correctly handles this base case, where the single station is its own grid and can only be online or offline.
Station IDs are 1-based (from 1 to c), which can lead to off-by-one errors with 0-indexed arrays.
How to Handle:
The implementation must consistently adjust for the 1-based indexing, typically by using arrays of size c+1 or by subtracting 1 from IDs.
The `queries` input array is empty.
How to Handle:
The solution should gracefully handle this by returning an empty results array, as there are no type 1 queries to process.