LeetCode 1168 - Optimize Water Distribution in a Village
The problem asks us to supply water to all houses in a village in the most cost-effective way. Each house has two options: either build a well in the house with a fixed cost, or connect the house to another house via pipes, where each pipe has its own construction cost.
Difficulty: 🔴 Hard
Topics: Union-Find, Graph Theory, Heap (Priority Queue), Minimum Spanning Tree
Solution
Problem Understanding
The problem asks us to supply water to all houses in a village in the most cost-effective way. Each house has two options: either build a well in the house with a fixed cost, or connect the house to another house via pipes, where each pipe has its own construction cost. The goal is to find a combination of wells and pipes that minimizes the total cost of supplying water to all houses.
The inputs are as follows: n is the number of houses, wells is an array where wells[i - 1] represents the cost to build a well in house i, and pipes is a list of triplets [house1, house2, cost] indicating the bidirectional pipe construction cost between house1 and house2. The output is a single integer representing the minimum total cost to supply water.
Constraints indicate that n and the number of pipes can go up to 10,000 and costs can be as high as 100,000. This tells us that brute-force approaches that explore all combinations of wells and pipes are infeasible. Important edge cases include scenarios where building a well is cheaper than any pipe connection, or where multiple pipes connect the same pair of houses but with different costs.
Approaches
A brute-force approach would consider every combination of building wells and connecting pipes. For each subset of houses with wells, we would then attempt all possible pipe configurations to supply the remaining houses. While this is guaranteed to find the minimum cost, it is computationally infeasible due to combinatorial explosion, especially since the number of houses and pipes can reach 10,000.
The key insight for an optimal solution comes from graph theory. We can treat the problem as finding a Minimum Spanning Tree (MST). We introduce a virtual node representing a water source and connect it to each house with an edge weight equal to the well construction cost. Then, we treat all pipes as regular edges between houses. Using either Kruskal's algorithm with Union-Find or Prim's algorithm with a priority queue, we can efficiently find the MST, which represents the minimal combination of wells and pipes to supply water to all houses.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n * m) | O(n + m) | Explore all well and pipe combinations, infeasible for n ~ 10^4 |
| Optimal (MST) | O((n + m) log (n + m)) | O(n + m) | Treat wells as edges to virtual node, use Kruskal's or Prim's algorithm |
Algorithm Walkthrough
- Introduce a virtual node 0 to represent the water source. Connect this node to every house
iwith an edge weight equal towells[i-1]. - Treat the original
pipesarray as edges between houses. - Combine all edges into a single list: edges from virtual node to each house plus all pipes.
- Sort all edges by cost in ascending order. This ensures that the MST algorithm considers cheaper connections first.
- Initialize a Union-Find (Disjoint Set Union) data structure to track connected components.
- Iterate through the sorted edges. For each edge, if the two nodes are not already connected, add the edge to the MST and union their sets. Accumulate the cost.
- Continue until all houses are connected to the water supply. At this point, the accumulated cost is the minimum total cost.
Why it works: The MST guarantees the minimum total weight to connect all nodes in a graph. By treating wells as edges from a virtual source node, the algorithm naturally chooses the cheaper option between building a well and connecting with pipes.
Python Solution
from typing import List
class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x: int, y: int) -> bool:
root_x, root_y = self.find(x), self.find(y)
if root_x == root_y:
return False
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
return True
class Solution:
def minCostToSupplyWater(self, n: int, wells: List[int], pipes: List[List[int]]) -> int:
edges = [[0, i + 1, wells[i]] for i in range(n)]
edges.extend(pipes)
edges.sort(key=lambda x: x[2])
uf = UnionFind(n + 1)
total_cost = 0
for u, v, cost in edges:
if uf.union(u, v):
total_cost += cost
return total_cost
The Python solution first constructs edges from the virtual node to every house and combines them with the given pipes. Sorting ensures we process the cheapest connections first. Union-Find manages connectivity and prevents cycles while accumulating the total minimum cost.
Go Solution
package main
import "sort"
type UnionFind struct {
parent []int
rank []int
}
func NewUnionFind(n int) *UnionFind {
parent := make([]int, n)
rank := make([]int, n)
for i := 0; i < n; i++ {
parent[i] = i
}
return &UnionFind{parent, rank}
}
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x])
}
return uf.parent[x]
}
func (uf *UnionFind) Union(x, y int) bool {
rootX, rootY := uf.Find(x), uf.Find(y)
if rootX == rootY {
return false
}
if uf.rank[rootX] < uf.rank[rootY] {
uf.parent[rootX] = rootY
} else if uf.rank[rootX] > uf.rank[rootY] {
uf.parent[rootY] = rootX
} else {
uf.parent[rootY] = rootX
uf.rank[rootX]++
}
return true
}
func minCostToSupplyWater(n int, wells []int, pipes [][]int) int {
edges := make([][]int, n)
for i := 0; i < n; i++ {
edges[i] = []int{0, i + 1, wells[i]}
}
edges = append(edges, pipes...)
sort.Slice(edges, func(i, j int) bool {
return edges[i][2] < edges[j][2]
})
uf := NewUnionFind(n + 1)
totalCost := 0
for _, edge := range edges {
if uf.Union(edge[0], edge[1]) {
totalCost += edge[2]
}
}
return totalCost
}
In Go, the implementation mirrors Python, with attention to slice allocation and sort function syntax. Union-Find is implemented as a struct with methods.
Worked Examples
Example 1
Input: n = 3, wells = [1,2,2], pipes = [[1,2,1],[2,3,1]]
Edges after adding virtual node:
[[0,1,1],[0,2,2],[0,3,2],[1,2,1],[2,3,1]]
Sorted edges:
[[0,1,1],[1,2,1],[2,3,1],[0,2,2],[0,3,2]]
Step-by-step Union-Find:
| Edge | Union? | Total Cost | Connected Houses |
|---|---|---|---|
| [0,1,1] | Yes | 1 | 1 |
| [1,2,1] | Yes | 2 | 1,2 |
| [2,3,1] | Yes | 3 | 1,2,3 |
| [0,2,2] | No | 3 | already connected |
| [0,3,2] | No | 3 | already connected |
Output: 3
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O((n + m) log (n + m)) | Sorting all edges dominates, union-find operations are near-constant amortized |
| Space | O(n + m) | Storage for edges and union-find parent/rank arrays |
Sorting is the bottleneck, and union-find with path compression ensures efficient connectivity checks.
Test Cases
# Example test cases
assert Solution().minCostToSupplyWater(3, [1,2,2], [[1,2,1],[2,3,1]]) == 3 # Example 1
assert Solution().minCostToSupplyWater(2, [1,1], [[1,2,1],[1,2,2]]) == 2 # Example 2
# Boundary tests
assert Solution().minCostToSupplyWater(2, [0,0], [[1,2,100]]) == 0 # Zero cost wells
assert Solution