Facebook Pixel

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 element edges[i] = [ui, vi, wi] represents an edge between vertices ui and vi with weight wi

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 to ti if such a walk exists
  • -1 if no walk exists between si and ti

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 is 0 (no edges traversed)
  • If si and ti are in different connected components, return -1
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Finding which nodes belong to the same connected component (this is where Union-Find comes in)
  2. Computing the AND of all edge weights within each component
  3. 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 is 0
  • If s and t 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 using uf.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, return 0 (no edges traversed)
  • Find the roots of s and t using uf.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)) where E is the number of edges and α is the inverse Ackermann function
  • Computing AND values: O(E × α(n))
  • Processing queries: O(Q × α(n)) where Q 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 Evaluator

Example 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 1

    Component 0-1: {0, 1}
    Component 2: {2}
    Component 3: {3}
  • Edge [1, 2, 5]: Union vertices 1 and 2

    Component 0-1-2: {0, 1, 2}
    Component 3: {3}
  • Edge [0, 3, 3]: Union vertices 0 and 3

    Component 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 each union operation takes O(α(n)) amortized time due to path compression in find
  • Processing edges (second loop): O(m × α(n)) where each edge requires a find operation to get the root and perform bitwise AND
  • Processing queries: O(q × α(n)) where each query calls function f which performs at most two find 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 array p and size array size
  • 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 in 0, regardless of the actual edge weights
  • If initialized with 0, all queries would incorrectly return 0 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 to 1 (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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More