3108. Minimum Cost Walk in Weighted Graph
Problem Description
You have an undirected weighted graph with n
vertices labeled from 0
to n - 1
.
The graph is defined by:
- An integer
n
representing the number of vertices - An array
edges
where each elementedges[i] = [ui, vi, wi]
represents an edge between verticesui
andvi
with weightwi
A walk on the graph is a sequence of vertices and edges where:
- The walk starts and ends with a vertex
- Each edge connects consecutive vertices in the sequence
- The same edge or vertex can be visited multiple times
The cost of a walk from node u
to node v
is calculated as the bitwise AND of all edge weights traversed during the walk. For example, if the edge weights encountered are w0, w1, w2, ..., wk
, then the cost = w0 & w1 & w2 & ... & wk
.
You're given a 2D array query
where each query[i] = [si, ti]
asks for the minimum cost of any walk from vertex si
to vertex ti
.
For each query, you need to find:
- The minimum cost of a walk from
si
toti
if such a walk exists -1
if no walk exists betweensi
andti
Return an array answer
where answer[i]
contains the result for query[i]
.
Key Insights:
- Since we can revisit edges and vertices, if two nodes are in the same connected component, we can traverse all edges in that component
- The bitwise AND operation only decreases or stays the same when applied to positive integers
- Therefore, the minimum cost for any walk between two connected nodes would be the AND of all edge weights in their connected component
- If
si = ti
, the cost is0
(no edges traversed) - If
si
andti
are in different connected components, return-1
Intuition
Let's think about what happens when we take a walk between two nodes. The key observation is that we can revisit edges and vertices as many times as we want. This means if nodes s
and t
are connected, we can potentially use ANY edge in their connected component as part of our walk.
Consider the bitwise AND operation. When we AND multiple numbers together, the result can only get smaller or stay the same - it never increases. For example, 5 & 3 = 1
, and if we continue 1 & 7 = 1
. Each bit position in the result can only be 1
if that bit is 1
in ALL the numbers being ANDed together.
Since we want to minimize the cost and we can use any edge in the connected component, we should use ALL edges in the component. Why? Because using more edges in the AND operation can only make the result smaller or keep it the same.
This leads us to a crucial insight: the minimum cost for any walk between two connected nodes is simply the AND of ALL edge weights in their connected component. It doesn't matter what specific path we take - as long as we can include all edges in the component (which we can, since we can revisit edges), we'll get the minimum possible cost.
Now the problem transforms into:
- Finding which nodes belong to the same connected component (this is where Union-Find comes in)
- Computing the AND of all edge weights within each component
- For each query, checking if the two nodes are in the same component and returning the precomputed AND value
Special cases:
- If
s = t
, we don't need to traverse any edges, so the cost is0
- If
s
andt
are in different components, there's no walk between them, so we return-1
Learn more about Union Find and Graph patterns.
Solution Approach
The solution uses Union-Find (Disjoint Set Union) data structure to efficiently manage connected components and compute the minimum cost for each component.
Step 1: Build Connected Components
First, we initialize a Union-Find structure with n
nodes. The Union-Find helps us:
- Identify which nodes belong to the same connected component
- Merge components when we process edges
For each edge (u, v, w)
, we union nodes u
and v
to put them in the same connected component. The union operation uses union by size optimization to keep the tree balanced.
Step 2: Compute Component-wise AND Values
We create an array g
where g[i]
will store the bitwise AND of all edge weights in the component whose root is i
. Initially, all values are set to -1
(all bits set to 1).
Then we traverse each edge (u, v, w)
again:
- Find the root of the component containing
u
usinguf.find(u)
- Update
g[root] &= w
to accumulate the AND of all edge weights in this component
Note that we only need to find the root of one endpoint since both endpoints are already in the same component after Step 1.
Step 3: Process Queries
For each query (s, t)
:
- If
s == t
, return0
(no edges traversed) - Find the roots of
s
andt
usinguf.find()
- If they have the same root (same component), return
g[root]
(the precomputed AND value) - If they have different roots (different components), return
-1
(no path exists)
Time Complexity:
- Building Union-Find:
O(E × α(n))
whereE
is the number of edges andα
is the inverse Ackermann function - Computing AND values:
O(E × α(n))
- Processing queries:
O(Q × α(n))
whereQ
is the number of queries - Overall:
O((E + Q) × α(n))
, which is nearly linear
Space Complexity: O(n)
for the Union-Find structure and the g
array
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Given:
n = 4
vertices (labeled 0, 1, 2, 3)edges = [[0, 1, 7], [1, 2, 5], [0, 3, 3]]
query = [[0, 2], [1, 3], [2, 3]]
Step 1: Build Connected Components using Union-Find
Initial state: Each vertex is its own component
Component 0: {0} Component 1: {1} Component 2: {2} Component 3: {3}
Process edges:
-
Edge
[0, 1, 7]
: Union vertices 0 and 1Component 0-1: {0, 1} Component 2: {2} Component 3: {3}
-
Edge
[1, 2, 5]
: Union vertices 1 and 2Component 0-1-2: {0, 1, 2} Component 3: {3}
-
Edge
[0, 3, 3]
: Union vertices 0 and 3Component 0-1-2-3: {0, 1, 2, 3}
All vertices are now in the same connected component!
Step 2: Compute Component-wise AND Values
Initialize g
array with -1 (all bits set): g = [-1, -1, -1, -1]
Process each edge and update the AND value for its component's root:
-
Edge
[0, 1, 7]
:- Find root of vertex 0 → root is 0
- Update:
g[0] = -1 & 7 = 7
(binary: 111)
-
Edge
[1, 2, 5]
:- Find root of vertex 1 → root is 0
- Update:
g[0] = 7 & 5 = 5
(binary: 111 & 101 = 101)
-
Edge
[0, 3, 3]
:- Find root of vertex 0 → root is 0
- Update:
g[0] = 5 & 3 = 1
(binary: 101 & 011 = 001)
Final AND value for the component: g[0] = 1
Step 3: Process Queries
Query 1: [0, 2]
- s = 0, t = 2
- s ≠ t, so check if they're connected
- Find root of 0 → 0
- Find root of 2 → 0
- Same root! Return
g[0] = 1
Query 2: [1, 3]
- s = 1, t = 3
- s ≠ t, so check if they're connected
- Find root of 1 → 0
- Find root of 3 → 0
- Same root! Return
g[0] = 1
Query 3: [2, 3]
- s = 2, t = 3
- s ≠ t, so check if they're connected
- Find root of 2 → 0
- Find root of 3 → 0
- Same root! Return
g[0] = 1
Final Answer: [1, 1, 1]
Why this works:
Since all vertices are in the same component, any walk between any two vertices can potentially traverse all three edges. The minimum possible cost is achieved by including all edges in the walk (which we can do by revisiting them), giving us 7 & 5 & 3 = 1
. This is the minimum cost for any walk between any two vertices in this component.
Solution Implementation
1class UnionFind:
2 def __init__(self, n: int):
3 """Initialize Union-Find data structure with n elements."""
4 # Each element is initially its own parent
5 self.parent = list(range(n))
6 # Track size of each component for union by rank optimization
7 self.component_size = [1] * n
8
9 def find(self, x: int) -> int:
10 """Find the root of element x with path compression."""
11 if self.parent[x] != x:
12 # Path compression: make all nodes point directly to root
13 self.parent[x] = self.find(self.parent[x])
14 return self.parent[x]
15
16 def union(self, node_a: int, node_b: int) -> bool:
17 """
18 Unite the components containing node_a and node_b.
19 Returns True if they were in different components, False otherwise.
20 """
21 root_a = self.find(node_a)
22 root_b = self.find(node_b)
23
24 # Already in same component
25 if root_a == root_b:
26 return False
27
28 # Union by size: attach smaller tree under larger tree
29 if self.component_size[root_a] > self.component_size[root_b]:
30 self.parent[root_b] = root_a
31 self.component_size[root_a] += self.component_size[root_b]
32 else:
33 self.parent[root_a] = root_b
34 self.component_size[root_b] += self.component_size[root_a]
35
36 return True
37
38
39class Solution:
40 def minimumCost(
41 self, n: int, edges: List[List[int]], query: List[List[int]]
42 ) -> List[int]:
43 """
44 Find minimum cost for each query where cost is the bitwise AND
45 of all edge weights in any path between two nodes.
46
47 Args:
48 n: Number of nodes in the graph
49 edges: List of [u, v, weight] representing edges
50 query: List of [start, end] pairs to find minimum cost between
51
52 Returns:
53 List of minimum costs for each query
54 """
55 # Initialize component weights array (start with all bits set)
56 component_weights = [-1] * n
57
58 # Build connected components using Union-Find
59 union_find = UnionFind(n)
60 for source_node, dest_node, _ in edges:
61 union_find.union(source_node, dest_node)
62
63 # Calculate the bitwise AND of all edge weights for each component
64 for source_node, _, edge_weight in edges:
65 component_root = union_find.find(source_node)
66 # Accumulate weights using bitwise AND
67 component_weights[component_root] &= edge_weight
68
69 def calculate_query_cost(start_node: int, end_node: int) -> int:
70 """
71 Calculate minimum cost between two nodes.
72
73 Returns:
74 0 if nodes are the same
75 Component weight if nodes are in same component
76 -1 if nodes are in different components (no path exists)
77 """
78 # Same node - no edges needed
79 if start_node == end_node:
80 return 0
81
82 # Find roots of both nodes
83 start_root = union_find.find(start_node)
84 end_root = union_find.find(end_node)
85
86 # If in same component, return the AND of all edges in component
87 # Otherwise return -1 (no path exists)
88 return component_weights[start_root] if start_root == end_root else -1
89
90 # Process all queries and return results
91 return [calculate_query_cost(start, end) for start, end in query]
92
1/**
2 * Union-Find (Disjoint Set Union) data structure with path compression and union by size
3 */
4class UnionFind {
5 private final int[] parent; // parent[i] stores the parent of node i
6 private final int[] componentSize; // componentSize[i] stores the size of component with root i
7
8 /**
9 * Initialize Union-Find structure with n nodes (0 to n-1)
10 * Initially, each node is its own parent (separate component)
11 */
12 public UnionFind(int n) {
13 parent = new int[n];
14 componentSize = new int[n];
15 for (int i = 0; i < n; i++) {
16 parent[i] = i; // Each node is initially its own parent
17 componentSize[i] = 1; // Each component initially has size 1
18 }
19 }
20
21 /**
22 * Find the root of the component containing node x
23 * Uses path compression to optimize future queries
24 */
25 public int find(int x) {
26 if (parent[x] != x) {
27 parent[x] = find(parent[x]); // Path compression: directly connect x to root
28 }
29 return parent[x];
30 }
31
32 /**
33 * Union two components containing nodes a and b
34 * Uses union by size to keep the tree balanced
35 * Returns true if union was performed, false if already in same component
36 */
37 public boolean union(int a, int b) {
38 int rootA = find(a);
39 int rootB = find(b);
40
41 // Already in the same component
42 if (rootA == rootB) {
43 return false;
44 }
45
46 // Union by size: attach smaller tree to larger tree
47 if (componentSize[rootA] > componentSize[rootB]) {
48 parent[rootB] = rootA;
49 componentSize[rootA] += componentSize[rootB];
50 } else {
51 parent[rootA] = rootB;
52 componentSize[rootB] += componentSize[rootA];
53 }
54 return true;
55 }
56
57 /**
58 * Get the size of the component containing node x
59 */
60 public int size(int x) {
61 return componentSize[find(x)];
62 }
63}
64
65/**
66 * Solution for finding minimum cost paths in a graph with bitwise AND edge weights
67 */
68class Solution {
69 private UnionFind unionFind;
70 private int[] componentMinCost; // Stores the bitwise AND of all edges in each component
71
72 /**
73 * Calculate minimum cost for each query
74 * @param n Number of nodes in the graph
75 * @param edges Array of edges where each edge is [from, to, weight]
76 * @param query Array of queries where each query is [start, end]
77 * @return Array of minimum costs for each query
78 */
79 public int[] minimumCost(int n, int[][] edges, int[][] query) {
80 // Initialize Union-Find and build connected components
81 unionFind = new UnionFind(n);
82 for (int[] edge : edges) {
83 unionFind.union(edge[0], edge[1]);
84 }
85
86 // Initialize component costs with -1 (all bits set to 1)
87 componentMinCost = new int[n];
88 Arrays.fill(componentMinCost, -1);
89
90 // Calculate bitwise AND of all edge weights for each component
91 for (int[] edge : edges) {
92 int componentRoot = unionFind.find(edge[0]);
93 componentMinCost[componentRoot] &= edge[2]; // Bitwise AND with edge weight
94 }
95
96 // Process each query
97 int queryCount = query.length;
98 int[] result = new int[queryCount];
99 for (int i = 0; i < queryCount; i++) {
100 int start = query[i][0];
101 int end = query[i][1];
102 result[i] = calculateMinimumCost(start, end);
103 }
104
105 return result;
106 }
107
108 /**
109 * Calculate minimum cost from node u to node v
110 * @param u Start node
111 * @param v End node
112 * @return 0 if u equals v, component's minimum cost if connected, -1 if not connected
113 */
114 private int calculateMinimumCost(int u, int v) {
115 // Same node: cost is 0
116 if (u == v) {
117 return 0;
118 }
119
120 // Check if nodes are in the same component
121 int rootU = unionFind.find(u);
122 int rootV = unionFind.find(v);
123
124 // If in same component, return the component's minimum cost
125 // Otherwise, return -1 (not connected)
126 return rootU == rootV ? componentMinCost[rootU] : -1;
127 }
128}
129
1class UnionFind {
2public:
3 // Initialize Union-Find structure with n elements
4 UnionFind(int n) {
5 parent = vector<int>(n);
6 componentSize = vector<int>(n, 1);
7 // Initialize each element as its own parent (self-loop)
8 iota(parent.begin(), parent.end(), 0);
9 }
10
11 // Unite two elements into the same component
12 // Returns true if they were in different components, false otherwise
13 bool unite(int a, int b) {
14 int rootA = find(a);
15 int rootB = find(b);
16
17 // Already in the same component
18 if (rootA == rootB) {
19 return false;
20 }
21
22 // Union by size: attach smaller tree under root of larger tree
23 if (componentSize[rootA] > componentSize[rootB]) {
24 parent[rootB] = rootA;
25 componentSize[rootA] += componentSize[rootB];
26 } else {
27 parent[rootA] = rootB;
28 componentSize[rootB] += componentSize[rootA];
29 }
30 return true;
31 }
32
33 // Find the root of the component containing element x
34 // Uses path compression for optimization
35 int find(int x) {
36 if (parent[x] != x) {
37 parent[x] = find(parent[x]); // Path compression
38 }
39 return parent[x];
40 }
41
42 // Get the size of the component containing element x
43 int getSize(int x) {
44 return componentSize[find(x)];
45 }
46
47private:
48 vector<int> parent; // Parent array for Union-Find
49 vector<int> componentSize; // Size of each component
50};
51
52class Solution {
53public:
54 vector<int> minimumCost(int n, vector<vector<int>>& edges, vector<vector<int>>& query) {
55 // Initialize component weights array with all bits set (-1)
56 componentWeights = vector<int>(n, -1);
57 unionFind = new UnionFind(n);
58
59 // Build connected components by uniting nodes connected by edges
60 for (auto& edge : edges) {
61 unionFind->unite(edge[0], edge[1]);
62 }
63
64 // Calculate bitwise AND of all edge weights in each component
65 for (auto& edge : edges) {
66 int componentRoot = unionFind->find(edge[0]);
67 componentWeights[componentRoot] &= edge[2];
68 }
69
70 // Process queries and build result array
71 vector<int> result;
72 for (auto& q : query) {
73 result.push_back(calculateQueryCost(q[0], q[1]));
74 }
75
76 return result;
77 }
78
79private:
80 UnionFind* unionFind; // Union-Find data structure
81 vector<int> componentWeights; // Bitwise AND of all edge weights in each component
82
83 // Calculate the cost for a query from node u to node v
84 int calculateQueryCost(int u, int v) {
85 // Same node, cost is 0
86 if (u == v) {
87 return 0;
88 }
89
90 // Find the root components of both nodes
91 int rootU = unionFind->find(u);
92 int rootV = unionFind->find(v);
93
94 // If in same component, return the component's weight
95 // Otherwise, nodes are not connected, return -1
96 return (rootU == rootV) ? componentWeights[rootU] : -1;
97 }
98};
99
1// Parent array for Union-Find data structure
2let parent: number[];
3// Size array to track component sizes for union by rank optimization
4let componentSize: number[];
5
6/**
7 * Initialize Union-Find data structure
8 * @param n - Number of nodes
9 */
10function initializeUnionFind(n: number): void {
11 // Each node is initially its own parent
12 parent = Array(n).fill(0).map((_, index) => index);
13 // Each component initially has size 1
14 componentSize = Array(n).fill(1);
15}
16
17/**
18 * Find the root parent of a node with path compression
19 * @param x - Node to find root for
20 * @returns Root parent of the node
21 */
22function find(x: number): number {
23 // Path compression: make all nodes point directly to root
24 if (parent[x] !== x) {
25 parent[x] = find(parent[x]);
26 }
27 return parent[x];
28}
29
30/**
31 * Unite two components using union by size
32 * @param a - First node
33 * @param b - Second node
34 * @returns true if union was performed, false if already in same component
35 */
36function union(a: number, b: number): boolean {
37 // Find roots of both nodes
38 const rootA = find(a);
39 const rootB = find(b);
40
41 // Already in same component
42 if (rootA === rootB) {
43 return false;
44 }
45
46 // Union by size: attach smaller tree under larger tree
47 if (componentSize[rootA] > componentSize[rootB]) {
48 parent[rootB] = rootA;
49 componentSize[rootA] += componentSize[rootB];
50 } else {
51 parent[rootA] = rootB;
52 componentSize[rootB] += componentSize[rootA];
53 }
54
55 return true;
56}
57
58/**
59 * Get the size of the component containing node x
60 * @param x - Node to check
61 * @returns Size of the component
62 */
63function getSize(x: number): number {
64 return componentSize[find(x)];
65}
66
67/**
68 * Find minimum cost path between nodes in a graph
69 * @param n - Number of nodes
70 * @param edges - Array of edges [u, v, weight]
71 * @param query - Array of queries [start, end]
72 * @returns Array of minimum costs for each query
73 */
74function minimumCost(n: number, edges: number[][], query: number[][]): number[] {
75 // Initialize Union-Find structure
76 initializeUnionFind(n);
77
78 // Array to store bitwise AND result for each component
79 const componentWeight: number[] = Array(n).fill(-1);
80
81 // Build connected components by unioning all edges
82 for (const [nodeU, nodeV, _] of edges) {
83 union(nodeU, nodeV);
84 }
85
86 // Calculate bitwise AND of all edge weights in each component
87 for (const [nodeU, _, weight] of edges) {
88 const rootNode = find(nodeU);
89 componentWeight[rootNode] &= weight;
90 }
91
92 /**
93 * Helper function to find minimum cost between two nodes
94 * @param startNode - Starting node
95 * @param endNode - Ending node
96 * @returns Minimum cost or -1 if not connected
97 */
98 const findMinimumCost = (startNode: number, endNode: number): number => {
99 // Same node has zero cost
100 if (startNode === endNode) {
101 return 0;
102 }
103
104 // Check if nodes are in same component
105 const rootStart = find(startNode);
106 const rootEnd = find(endNode);
107
108 // If in same component, return component's weight, else -1
109 return rootStart === rootEnd ? componentWeight[rootStart] : -1;
110 };
111
112 // Process each query and return results
113 return query.map(([startNode, endNode]) => findMinimumCost(startNode, endNode));
114}
115
Time and Space Complexity
Time Complexity: O((n + m + q) × α(n))
The time complexity breaks down as follows:
- Initialization of UnionFind:
O(n)
to create parent and size arrays - Processing edges (first loop):
O(m × α(n))
where eachunion
operation takesO(α(n))
amortized time due to path compression infind
- Processing edges (second loop):
O(m × α(n))
where each edge requires afind
operation to get the root and perform bitwise AND - Processing queries:
O(q × α(n))
where each query calls functionf
which performs at most twofind
operations
The total time complexity is O(n + m × α(n) + m × α(n) + q × α(n)) = O((n + m + q) × α(n))
, where α(n)
is the inverse Ackermann function, which grows extremely slowly and is effectively constant for all practical values of n
.
Space Complexity: O(n)
The space complexity consists of:
- UnionFind data structure:
O(n)
for storing parent arrayp
and size arraysize
- Array
g
:O(n)
for storing the bitwise AND results for each component root - Auxiliary space:
O(1)
for variables and function call stack (with path compression, the recursion depth is bounded byα(n)
)
The total space complexity is O(n) + O(n) + O(1) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Component Weight Initialization
A critical pitfall is initializing the component weights array with 0
instead of -1
. This mistake completely breaks the algorithm's logic.
Why it's wrong:
- Bitwise AND with
0
always results in0
, regardless of the actual edge weights - If initialized with
0
, all queries would incorrectly return0
for connected components
Incorrect Implementation:
# WRONG: Starting with 0 component_weights = [0] * n # This will make all results 0! # Later when accumulating: component_weights[root] &= edge_weight # Always results in 0
Correct Implementation:
# CORRECT: Starting with -1 (all bits set to 1) component_weights = [-1] * n # Later when accumulating: component_weights[root] &= edge_weight # Properly accumulates AND values
Why -1
works:
- In binary,
-1
is represented as all bits set to1
(in two's complement) - When we AND with
-1
, we get the original value:x & -1 = x
- This allows us to properly accumulate the bitwise AND of all edge weights
2. Processing Component Weights Before Union-Find is Complete
Another common mistake is trying to calculate component weights while building the Union-Find structure, rather than in two separate passes.
Incorrect Approach:
# WRONG: Trying to do both at once for source, dest, weight in edges: union_find.union(source, dest) root = union_find.find(source) component_weights[root] &= weight # Root might change after more unions!
Problem: The root of a component can change as more edges are processed and components merge. This leads to weights being accumulated in the wrong indices.
Correct Approach:
# CORRECT: Two separate passes # First pass: Build all connections for source, dest, _ in edges: union_find.union(source, dest) # Second pass: Calculate weights after structure is stable for source, _, weight in edges: root = union_find.find(source) component_weights[root] &= weight
3. Forgetting the Self-Loop Case
Failing to handle the case where start_node == end_node
is a subtle but important oversight.
Why it matters:
- When start and end are the same node, no edges are traversed
- The bitwise AND of an empty set of edges should be considered as having no effect on the result
- The problem specifies this should return
0
Correct handling:
if start_node == end_node: return 0 # No edges traversed, cost is 0
Without this check, the code would incorrectly return the component's weight even for self-loops, which violates the problem's requirements.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
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
Want a Structured Path to Master System Design Too? Don’t Miss This!