Facebook Pixel

1697. Checking Existence of Edge Length Limited Paths

Problem Description

You are given an undirected graph with n nodes and a list of edges edgeList. Each edge is represented as edgeList[i] = [ui, vi, disi], which means there is an edge between nodes ui and vi with a distance (or weight) of disi. Note that there can be multiple edges between the same pair of nodes.

You are also given an array of queries queries, where each query is represented as queries[j] = [pj, qj, limitj]. For each query, you need to determine if there exists a path between nodes pj and qj such that every edge on that path has a distance strictly less than limitj.

Your task is to return a boolean array answer where:

  • answer.length == queries.length
  • answer[j] is true if there exists a valid path for queries[j] (a path from pj to qj where all edges have distances < limitj)
  • answer[j] is false otherwise

The key insight is that this problem asks about path existence with edge weight constraints. The solution uses an offline query processing technique combined with Union-Find data structure. By sorting both edges and queries by their weight/limit values, we can efficiently process queries by incrementally adding edges to our Union-Find structure. For each query with limit L, we add all edges with weight less than L to the Union-Find, then check if the two query nodes are connected.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The challenge here is that we need to answer multiple queries about path existence with different edge weight limits. A naive approach would be to build a new graph for each query containing only edges below the limit, then check connectivity - but this would be inefficient.

The key observation is that if we have a path between two nodes using edges with weights less than some limit L1, then we automatically have a path for any larger limit L2 > L1. This monotonic property suggests we can process queries in increasing order of their limits.

Think of it like gradually "activating" edges as we increase the limit threshold. Start with no edges (limit = 0), then progressively add edges as the limit increases. For a query with limit L, we only need edges with weight strictly less than L.

This leads us to the following approach:

  1. Sort edges by weight in ascending order
  2. Sort queries by limit in ascending order (but remember their original positions for the answer)
  3. Process queries one by one, and for each query:
    • Add all edges with weight less than the current limit to our graph
    • Check if the two query nodes are connected

For connectivity checking, Union-Find is perfect because:

  • We only need to add edges (union operation)
  • We need to check if two nodes are in the same connected component (find operation)
  • Both operations are nearly O(1) with path compression

By processing queries offline (not in their original order) and sorting them alongside edges, we ensure each edge is added at most once, making the solution efficient. The answer array is filled according to the original query indices to maintain the correct output order.

Learn more about Union Find, Graph, Two Pointers and Sorting patterns.

Solution Approach

The solution implements the offline query processing technique with Union-Find data structure. Here's how the implementation works:

1. Union-Find Setup:

  • Initialize a parent array p where p[i] = i for all nodes from 0 to n-1
  • Implement find(x) function with path compression to find the root of node x

2. Sort Operations:

  • Sort edgeList by edge weight in ascending order using edgeList.sort(key=lambda x: x[2])
  • Sort queries by limit value while preserving original indices using sorted(enumerate(queries), key=lambda x: x[1][2])
    • The enumeration keeps track of original query index i
    • Sorting is done based on x[1][2] which is the limit value

3. Process Queries in Order:

  • Initialize answer array ans with False values

  • Use pointer j to track the current position in sorted edgeList

  • For each sorted query (i, (a, b, limit)):

    Add eligible edges:

    while j < len(edgeList) and edgeList[j][2] < limit:
        u, v, _ = edgeList[j]
        p[find(u)] = find(v)  # Union operation
        j += 1
    • Add all edges with weight strictly less than current limit
    • Union the endpoints of each added edge

    Check connectivity:

    ans[i] = find(a) == find(b)
    • Check if nodes a and b belong to the same component
    • Store result at original index i in answer array

4. Return Results:

  • Return the ans array with results in the original query order

The time complexity is O(E log E + Q log Q + (E + Q) Γ— Ξ±(n)) where E is the number of edges, Q is the number of queries, and Ξ± is the inverse Ackermann function (practically constant). The space complexity is O(n) for the Union-Find parent array.

Ready to land your dream job?

Unlock your dream job with a 5-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 nodes (labeled 0, 1, 2, 3)
  • edgeList = [[0,1,2], [1,2,4], [2,3,1]]
    • Edge 0↔1 with weight 2
    • Edge 1↔2 with weight 4
    • Edge 2↔3 with weight 1
  • queries = [[0,2,3], [2,3,5], [0,3,2]]
    • Query 0: Can we go from 0 to 2 using only edges with weight < 3?
    • Query 1: Can we go from 2 to 3 using only edges with weight < 5?
    • Query 2: Can we go from 0 to 3 using only edges with weight < 2?

Step 1: Initialize Union-Find

parent = [0, 1, 2, 3]  (each node is its own parent initially)

Step 2: Sort edges by weight

sortedEdges = [[2,3,1], [0,1,2], [1,2,4]]
              (weight 1)  (weight 2)  (weight 4)

Step 3: Sort queries by limit (keeping original indices)

sortedQueries = [(2, [0,3,2]), (0, [0,2,3]), (1, [2,3,5])]
                 limit=2        limit=3        limit=5
                 (original index 2, 0, 1)

Step 4: Process queries in sorted order

Processing Query index 2: [0,3,2] with limit 2

  • Add edges with weight < 2: Only edge [2,3,1] qualifies
  • Union(2,3): parent becomes [0, 1, 3, 3]
  • Check find(0) == find(3)? β†’ 0 β‰  3 β†’ FALSE
  • ans[2] = False

Processing Query index 0: [0,2,3] with limit 3

  • Add edges with weight < 3: Edge [0,1,2] qualifies (edge [2,3,1] already added)
  • Union(0,1): parent becomes [1, 1, 3, 3]
  • Check find(0) == find(2)? β†’ 1 β‰  3 β†’ FALSE
  • ans[0] = False

Processing Query index 1: [2,3,5] with limit 5

  • Add edges with weight < 5: Edge [1,2,4] qualifies (others already added)
  • Union(1,2): parent becomes [1, 1, 1, 3] β†’ then [1, 1, 1, 1] after path compression
  • Check find(2) == find(3)? β†’ 1 == 1 β†’ TRUE
  • ans[1] = True

Step 5: Return answer in original order

ans = [False, True, False]

This means:

  • Query 0: No path from 0 to 2 using only edges < 3 (would need edge with weight 4)
  • Query 1: Path exists from 2 to 3 using edges < 5 (all edges qualify)
  • Query 2: No path from 0 to 3 using only edges < 2 (would need at least edge with weight 2)

Solution Implementation

1from typing import List
2
3class Solution:
4    def distanceLimitedPathsExist(
5        self, n: int, edgeList: List[List[int]], queries: List[List[int]]
6    ) -> List[bool]:
7        """
8        Determines if there exists a path between two nodes with all edge weights
9        less than a given limit for each query.
10      
11        Args:
12            n: Number of nodes in the graph (0 to n-1)
13            edgeList: List of edges [u, v, distance]
14            queries: List of queries [node_a, node_b, limit]
15      
16        Returns:
17            List of booleans indicating if a valid path exists for each query
18        """
19      
20        def find(node: int) -> int:
21            """
22            Find operation for Union-Find with path compression.
23            Returns the root parent of the given node.
24            """
25            if parent[node] != node:
26                # Path compression: make node point directly to root
27                parent[node] = find(parent[node])
28            return parent[node]
29      
30        # Initialize Union-Find parent array where each node is its own parent
31        parent = list(range(n))
32      
33        # Sort edges by weight in ascending order
34        edgeList.sort(key=lambda edge: edge[2])
35      
36        # Initialize result array
37        result = [False] * len(queries)
38      
39        # Process queries in ascending order of limit while preserving original indices
40        # enumerate(queries) gives (index, query), then sort by query's limit (index 2)
41        sorted_queries = sorted(enumerate(queries), key=lambda x: x[1][2])
42      
43        edge_index = 0
44      
45        for query_index, (node_a, node_b, distance_limit) in sorted_queries:
46            # Add all edges with weight less than current query's limit to Union-Find
47            while edge_index < len(edgeList) and edgeList[edge_index][2] < distance_limit:
48                u, v, _ = edgeList[edge_index]
49                # Union operation: connect the two components
50                parent[find(u)] = find(v)
51                edge_index += 1
52          
53            # Check if node_a and node_b are in the same connected component
54            result[query_index] = find(node_a) == find(node_b)
55      
56        return result
57
1class Solution {
2    // Parent array for Union-Find (Disjoint Set Union) data structure
3    private int[] parent;
4
5    /**
6     * Determines if there exists a path between two nodes with all edge weights less than a given limit.
7     * @param n Number of nodes in the graph (0 to n-1)
8     * @param edgeList Array of edges where each edge is [node1, node2, weight]
9     * @param queries Array of queries where each query is [node1, node2, weightLimit]
10     * @return Boolean array where ans[i] indicates if there's a path for queries[i]
11     */
12    public boolean[] distanceLimitedPathsExist(int n, int[][] edgeList, int[][] queries) {
13        // Initialize Union-Find structure with each node as its own parent
14        parent = new int[n];
15        for (int i = 0; i < n; i++) {
16            parent[i] = i;
17        }
18      
19        // Sort edges by weight in ascending order
20        Arrays.sort(edgeList, (edge1, edge2) -> edge1[2] - edge2[2]);
21      
22        int queryCount = queries.length;
23        boolean[] results = new boolean[queryCount];
24      
25        // Create array of query indices to sort queries without losing original order
26        Integer[] queryIndices = new Integer[queryCount];
27        for (int i = 0; i < queryCount; i++) {
28            queryIndices[i] = i;
29        }
30      
31        // Sort query indices based on query weight limits in ascending order
32        Arrays.sort(queryIndices, (idx1, idx2) -> queries[idx1][2] - queries[idx2][2]);
33      
34        int edgeIndex = 0;
35      
36        // Process queries in order of increasing weight limit
37        for (int currentQueryIndex : queryIndices) {
38            int sourceNode = queries[currentQueryIndex][0];
39            int targetNode = queries[currentQueryIndex][1];
40            int weightLimit = queries[currentQueryIndex][2];
41          
42            // Add all edges with weight less than current query's limit to Union-Find
43            while (edgeIndex < edgeList.length && edgeList[edgeIndex][2] < weightLimit) {
44                int nodeU = edgeList[edgeIndex][0];
45                int nodeV = edgeList[edgeIndex][1];
46              
47                // Union the two nodes by connecting their root parents
48                parent[find(nodeU)] = find(nodeV);
49                edgeIndex++;
50            }
51          
52            // Check if source and target nodes are connected (have same root parent)
53            results[currentQueryIndex] = (find(sourceNode) == find(targetNode));
54        }
55      
56        return results;
57    }
58
59    /**
60     * Finds the root parent of a node using path compression optimization.
61     * @param node The node whose root parent needs to be found
62     * @return The root parent of the node
63     */
64    private int find(int node) {
65        // Path compression: Make each node point directly to root
66        if (parent[node] != node) {
67            parent[node] = find(parent[node]);
68        }
69        return parent[node];
70    }
71}
72
1class Solution {
2public:
3    vector<bool> distanceLimitedPathsExist(int n, vector<vector<int>>& edgeList, vector<vector<int>>& queries) {
4        // Initialize Union-Find parent array where each node is its own parent
5        vector<int> parent(n);
6        iota(parent.begin(), parent.end(), 0);
7      
8        // Sort edges by weight in ascending order
9        sort(edgeList.begin(), edgeList.end(), [](const auto& edge1, const auto& edge2) { 
10            return edge1[2] < edge2[2]; 
11        });
12      
13        // Find function with path compression for Union-Find
14        function<int(int)> findRoot = [&](int node) -> int {
15            if (parent[node] != node) {
16                parent[node] = findRoot(parent[node]); // Path compression
17            }
18            return parent[node];
19        };
20      
21        int queryCount = queries.size();
22        vector<bool> result(queryCount);
23      
24        // Create array of query indices to process queries by weight limit
25        vector<int> queryIndices(queryCount);
26        iota(queryIndices.begin(), queryIndices.end(), 0);
27      
28        // Sort query indices by their distance limit in ascending order
29        sort(queryIndices.begin(), queryIndices.end(), [&](int idx1, int idx2) { 
30            return queries[idx1][2] < queries[idx2][2]; 
31        });
32      
33        int edgeIndex = 0;
34      
35        // Process queries in order of increasing distance limit
36        for (int queryIdx : queryIndices) {
37            int sourceNode = queries[queryIdx][0];
38            int targetNode = queries[queryIdx][1];
39            int distanceLimit = queries[queryIdx][2];
40          
41            // Add all edges with weight less than current query's limit to Union-Find
42            while (edgeIndex < edgeList.size() && edgeList[edgeIndex][2] < distanceLimit) {
43                int nodeU = edgeList[edgeIndex][0];
44                int nodeV = edgeList[edgeIndex][1];
45              
46                // Union operation: merge the two components
47                parent[findRoot(nodeU)] = findRoot(nodeV);
48                edgeIndex++;
49            }
50          
51            // Check if source and target are connected with edges under the limit
52            result[queryIdx] = (findRoot(sourceNode) == findRoot(targetNode));
53        }
54      
55        return result;
56    }
57};
58
1function distanceLimitedPathsExist(n: number, edgeList: number[][], queries: number[][]): boolean[] {
2    // Initialize Union-Find parent array where each node is its own parent
3    const parent: number[] = Array.from({ length: n }, (_, i) => i);
4  
5    // Sort edges by weight in ascending order
6    edgeList.sort((edge1, edge2) => edge1[2] - edge2[2]);
7  
8    // Find function with path compression for Union-Find
9    const findRoot = (node: number): number => {
10        if (parent[node] !== node) {
11            parent[node] = findRoot(parent[node]); // Path compression
12        }
13        return parent[node];
14    };
15  
16    const queryCount = queries.length;
17    const result: boolean[] = new Array(queryCount);
18  
19    // Create array of query indices to process queries by weight limit
20    const queryIndices: number[] = Array.from({ length: queryCount }, (_, i) => i);
21  
22    // Sort query indices by their distance limit in ascending order
23    queryIndices.sort((idx1, idx2) => queries[idx1][2] - queries[idx2][2]);
24  
25    let edgeIndex = 0;
26  
27    // Process queries in order of increasing distance limit
28    for (const queryIdx of queryIndices) {
29        const sourceNode = queries[queryIdx][0];
30        const targetNode = queries[queryIdx][1];
31        const distanceLimit = queries[queryIdx][2];
32      
33        // Add all edges with weight less than current query's limit to Union-Find
34        while (edgeIndex < edgeList.length && edgeList[edgeIndex][2] < distanceLimit) {
35            const nodeU = edgeList[edgeIndex][0];
36            const nodeV = edgeList[edgeIndex][1];
37          
38            // Union operation: merge the two components
39            parent[findRoot(nodeU)] = findRoot(nodeV);
40            edgeIndex++;
41        }
42      
43        // Check if source and target are connected with edges under the limit
44        result[queryIdx] = findRoot(sourceNode) === findRoot(targetNode);
45    }
46  
47    return result;
48}
49

Time and Space Complexity

Time Complexity: O(m Γ— log m + q Γ— log q + (m + q) Γ— Ξ±(n))

Breaking down the time complexity:

  • Sorting edgeList by weight takes O(m Γ— log m) where m is the number of edges
  • Sorting queries by their limit value takes O(q Γ— log q) where q is the number of queries
  • The main loop iterates through all q queries
  • The inner while loop processes each edge at most once across all iterations, contributing O(m) operations
  • Each union-find operation (find and union) takes O(Ξ±(n)) amortized time, where Ξ± is the inverse Ackermann function
  • Total union operations: at most m (one per edge)
  • Total find operations: at most 2m + 2q (two finds per union, two finds per query check)

Since Ξ±(n) grows extremely slowly and is effectively constant for practical values of n, the time complexity can be simplified to O(m Γ— log m + q Γ— log q) as stated in the reference answer.

Space Complexity: O(n + m + q)

Breaking down the space complexity:

  • The parent array p uses O(n) space
  • Storing the sorted edgeList uses O(m) space (though this could be considered as modifying input in-place)
  • The sorted queries with indices use O(q) space for the enumeration
  • The answer array ans uses O(q) space
  • The recursion stack for path compression in the worst case uses O(n) space before compression occurs

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Comparison Operator in Edge Addition

Pitfall: Using <= instead of < when comparing edge weights with the query limit.

# WRONG: This includes edges with weight equal to limit
while edge_index < len(edgeList) and edgeList[edge_index][2] <= distance_limit:
    u, v, _ = edgeList[edge_index]
    parent[find(u)] = find(v)
    edge_index += 1

Why it's wrong: The problem specifically states that every edge on the path must have distance strictly less than limitj. Including edges with weight equal to the limit violates this constraint.

Solution: Always use strict inequality <:

# CORRECT: Only includes edges with weight strictly less than limit
while edge_index < len(edgeList) and edgeList[edge_index][2] < distance_limit:
    u, v, _ = edgeList[edge_index]
    parent[find(u)] = find(v)
    edge_index += 1

2. Forgetting to Preserve Original Query Order

Pitfall: Sorting queries without tracking their original indices, leading to results in wrong positions.

# WRONG: Loses track of original query positions
sorted_queries = sorted(queries, key=lambda x: x[2])
result = []
for node_a, node_b, distance_limit in sorted_queries:
    # Process query...
    result.append(find(node_a) == find(node_b))  # Wrong order!

Solution: Use enumerate() to track original indices:

# CORRECT: Preserves original query indices
sorted_queries = sorted(enumerate(queries), key=lambda x: x[1][2])
result = [False] * len(queries)
for query_index, (node_a, node_b, distance_limit) in sorted_queries:
    # Process query...
    result[query_index] = find(node_a) == find(node_b)  # Correct position!

3. Resetting Union-Find Between Queries

Pitfall: Reinitializing the Union-Find structure for each query, which defeats the purpose of the offline processing optimization.

# WRONG: Inefficient approach that resets Union-Find for each query
for query in queries:
    parent = list(range(n))  # Reset Union-Find - WRONG!
    for edge in edgeList:
        if edge[2] < query[2]:
            # Add edge...

Solution: Maintain a single Union-Find structure throughout and process queries in sorted order:

# CORRECT: Initialize Union-Find once and reuse it
parent = list(range(n))  # Initialize only once
edge_index = 0
for query_index, (node_a, node_b, distance_limit) in sorted_queries:
    # Incrementally add edges without resetting
    while edge_index < len(edgeList) and edgeList[edge_index][2] < distance_limit:
        # Add edge...
        edge_index += 1

4. Incorrect Union Operation

Pitfall: Directly modifying parent array without using find operation, breaking the Union-Find structure.

# WRONG: Direct assignment without finding roots
parent[u] = v  # or parent[u] = parent[v]

Solution: Always union the roots of the components:

# CORRECT: Union the roots of both components
parent[find(u)] = find(v)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

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

Load More