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]
istrue
if there exists a valid path forqueries[j]
(a path frompj
toqj
where all edges have distances <limitj
)answer[j]
isfalse
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.
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:
- Sort edges by weight in ascending order
- Sort queries by limit in ascending order (but remember their original positions for the answer)
- 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
wherep[i] = i
for all nodes from0
ton-1
- Implement
find(x)
function with path compression to find the root of nodex
2. Sort Operations:
- Sort
edgeList
by edge weight in ascending order usingedgeList.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
- The enumeration keeps track of original query index
3. Process Queries in Order:
-
Initialize answer array
ans
withFalse
values -
Use pointer
j
to track the current position in sortededgeList
-
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
andb
belong to the same component - Store result at original index
i
in answer array
- Add all edges with weight strictly less than current
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 EvaluatorExample 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 takesO(m Γ log m)
wherem
is the number of edges - Sorting queries by their limit value takes
O(q Γ log q)
whereq
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
usesO(n)
space - Storing the sorted
edgeList
usesO(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
usesO(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)
Which of these properties could exist for a graph but not a tree?
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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Want a Structured Path to Master System Design Too? Donβt Miss This!