1724. Checking Existence of Edge Length Limited Paths II 🔒
Problem Description
You are given an undirected graph with n
nodes and a list of edges. Each edge is represented as [u, v, dis]
, where u
and v
are nodes connected by an edge with distance dis
. The graph may have multiple edges between the same pair of nodes and may not be fully connected.
You need to implement a class DistanceLimitedPathsExist
with two methods:
-
Constructor
DistanceLimitedPathsExist(int n, int[][] edgeList)
: Initializes the class with the graph structure, wheren
is the number of nodes andedgeList
contains all the edges. -
Query Method
boolean query(int p, int q, int limit)
: Returnstrue
if there exists a path from nodep
to nodeq
where every edge on the path has a distance strictly less thanlimit
. Returnsfalse
otherwise.
The solution uses a Persistent Union-Find data structure to efficiently handle queries. The approach works as follows:
-
Initialization Phase:
- Sort all edges by their distances in ascending order
- Process edges one by one, union-ing nodes with each edge's distance as a "version" timestamp
- The Persistent Union-Find maintains a history of when nodes became connected
-
Query Phase:
- For a query with limit
limit
, the Persistent Union-Find checks if nodesp
andq
belong to the same component when considering only edges with distance less thanlimit
- This is done by finding the root of each node at "time"
limit
- if they share the same root, a valid path exists
- For a query with limit
The key insight is that by sorting edges and using versioning in the Union-Find structure, we can efficiently determine connectivity at any distance threshold without rebuilding the graph for each query.
Intuition
The key observation is that we need to answer multiple queries about path existence with different distance limits. A naive approach would be to rebuild the graph for each query using only edges below the limit, then check connectivity - but this would be inefficient.
Let's think about what happens as we gradually increase the distance limit from 0 to infinity. Initially, no nodes are connected (limit = 0). As we increase the limit, edges start becoming "valid" for use in paths. When an edge with distance d
becomes valid, it potentially connects two previously disconnected components.
This suggests a timeline perspective: at each "moment" (distance value), the graph's connectivity structure changes. If we sort edges by distance and process them in order, we're essentially watching the graph evolve from completely disconnected to its final form.
For a query asking "can we go from p
to q
with edges < limit
?", we're really asking: "are p
and q
in the same connected component when we only consider edges with distance < limit
?"
This naturally leads to using Union-Find, which excels at tracking connected components. But standard Union-Find would require rebuilding for each query. The breakthrough is realizing we can make Union-Find "persistent" - recording WHEN each union operation happened.
By tagging each union with the edge's distance as a timestamp, we create a historical record. During a query with limit L
, we can ask the Union-Find to show us the state of connectivity "at time L
" - effectively traveling back in time to see which nodes were connected using only edges with distance < L
.
This transforms the problem from repeatedly building graphs to a single preprocessing step (sorting and union operations) followed by efficient O(log n) queries using the persistent structure.
Learn more about Union Find, Graph and Minimum Spanning Tree patterns.
Solution Approach
The solution implements a Persistent Union-Find data structure to efficiently handle connectivity queries with distance constraints.
Persistent Union-Find Implementation
The PersistentUnionFind
class maintains three key arrays:
p[]
: Parent pointers for each node (standard Union-Find)rank[]
: Rank/height of each tree for union by rank optimizationversion[]
: Stores the "timestamp" (edge distance) when a node's parent was changed
Find Operation with Time Travel:
def find(self, x, t=inf):
if self.p[x] == x or self.version[x] >= t:
return x
return self.find(self.p[x], t)
The find operation takes an additional parameter t
(time/distance limit). It stops traversing up the parent chain if:
- We reach a root node (
self.p[x] == x
) - We encounter a parent link created at or after time
t
(self.version[x] >= t
)
This effectively shows us the component structure as it existed before time t
.
Union Operation with Versioning:
def union(self, a, b, t):
pa, pb = self.find(a), self.find(b)
if pa == pb:
return False
if self.rank[pa] > self.rank[pb]:
self.version[pb] = t
self.p[pb] = pa
else:
self.version[pa] = t
self.p[pa] = pb
if self.rank[pa] == self.rank[pb]:
self.rank[pb] += 1
When uniting two components, the algorithm:
- Finds current roots of both nodes
- Uses union by rank to decide which root becomes the parent
- Crucially: Records the timestamp
t
in the version array for the node that changes parent
Main Class Implementation
Initialization:
def __init__(self, n: int, edgeList: List[List[int]]):
self.puf = PersistentUnionFind(n)
edgeList.sort(key=lambda x: x[2])
for u, v, dis in edgeList:
self.puf.union(u, v, dis)
- Creates a Persistent Union-Find with
n
nodes - Sorts edges by distance in ascending order - this is critical
- Processes edges sequentially, using each edge's distance as its timestamp
The sorting ensures that when we query with limit L
, all edges with distance < L
have been processed in chronological order.
Query Operation:
def query(self, p: int, q: int, limit: int) -> bool:
return self.puf.find(p, limit) == self.puf.find(q, limit)
To check if nodes p
and q
are connected using only edges with distance < limit
:
- Find the root of
p
at timelimit
- Find the root of
q
at timelimit
- If they have the same root, they're in the same component
The beauty of this approach is that each query runs in O(α(n)) time (nearly constant), where α is the inverse Ackermann function, while preprocessing takes O(E log E) time for sorting the edges.
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 how the Persistent Union-Find solution works.
Given:
- 4 nodes (labeled 0, 1, 2, 3)
- Edges:
[[0,1,2], [1,2,4], [2,3,1], [0,3,5]]
Step 1: Initialization and Sorting
First, we sort edges by distance:
[2,3,1] -> distance 1 [0,1,2] -> distance 2 [1,2,4] -> distance 4 [0,3,5] -> distance 5
Initial state - all nodes are their own parent:
Node: 0 1 2 3 Parent: 0 1 2 3 Version: ∞ ∞ ∞ ∞
Step 2: Processing Edges (Building History)
Process edge [2,3,1]
with timestamp 1:
- Find(2) = 2, Find(3) = 3 (different components)
- Union them: make 3's parent = 2
Node: 0 1 2 3 Parent: 0 1 2 2 Version: ∞ ∞ ∞ 1 ← Node 3 changed parent at time 1
Process edge [0,1,2]
with timestamp 2:
- Find(0) = 0, Find(1) = 1 (different components)
- Union them: make 1's parent = 0
Node: 0 1 2 3 Parent: 0 0 2 2 Version: ∞ 2 ∞ 1 ← Node 1 changed parent at time 2
Process edge [1,2,4]
with timestamp 4:
- Find(1) = 0, Find(2) = 2 (different components)
- Union them: make 2's parent = 0
Node: 0 1 2 3 Parent: 0 0 0 2 Version: ∞ 2 4 1 ← Node 2 changed parent at time 4
Process edge [0,3,5]
with timestamp 5:
- Find(0) = 0, Find(3) = 0 (already in same component!)
- No union needed
Step 3: Query Examples
Query(2, 3, limit=3): Can we go from node 2 to 3 using edges < 3?
- Find(2, time=3):
- Check parent[2] = 0, but version[2] = 4 ≥ 3
- So ignore this link and return 2
- Find(3, time=3):
- Check parent[3] = 2, version[3] = 1 < 3 ✓
- Continue to Find(2, time=3) = 2
- Result: Find(2,3) = 2, Find(3,3) = 2 → TRUE (same component)
Query(0, 2, limit=3): Can we go from node 0 to 2 using edges < 3?
- Find(0, time=3): Returns 0 (it's a root)
- Find(2, time=3):
- Check parent[2] = 0, but version[2] = 4 ≥ 3
- So ignore this link and return 2
- Result: Find(0,3) = 0, Find(2,3) = 2 → FALSE (different components)
Query(0, 2, limit=5): Can we go from node 0 to 2 using edges < 5?
- Find(0, time=5): Returns 0
- Find(2, time=5):
- Check parent[2] = 0, version[2] = 4 < 5 ✓
- Continue to Find(0, time=5) = 0
- Result: Find(0,5) = 0, Find(2,5) = 0 → TRUE (same component)
The key insight: by tracking WHEN each parent link was created, we can "time travel" to see the connectivity state at any distance threshold without rebuilding the graph!
Solution Implementation
1from typing import List
2from math import inf
3
4
5class PersistentUnionFind:
6 """
7 A persistent union-find data structure that maintains version history.
8 Each union operation is associated with a timestamp/version.
9 """
10
11 def __init__(self, n: int) -> None:
12 """
13 Initialize the persistent union-find structure.
14
15 Args:
16 n: Number of elements (nodes) in the structure
17 """
18 # Rank array for union by rank optimization
19 self.rank: List[int] = [0] * n
20 # Parent array - initially each element is its own parent
21 self.parent: List[int] = list(range(n))
22 # Version/timestamp when each element was last modified
23 # Initialize with infinity to indicate no modifications yet
24 self.version: List[float] = [inf] * n
25
26 def find(self, x: int, timestamp: float = inf) -> int:
27 """
28 Find the root of element x at a given timestamp.
29
30 Args:
31 x: The element to find the root for
32 timestamp: The version/timestamp to query at (default: infinity for latest)
33
34 Returns:
35 The root element of x at the given timestamp
36 """
37 # If x is its own parent, or the modification happened after the query timestamp,
38 # then x is the root at this timestamp
39 if self.parent[x] == x or self.version[x] >= timestamp:
40 return x
41 # Otherwise, recursively find the parent at the given timestamp
42 return self.find(self.parent[x], timestamp)
43
44 def union(self, a: int, b: int, timestamp: float) -> bool:
45 """
46 Union two elements at a given timestamp using union by rank.
47
48 Args:
49 a: First element to union
50 b: Second element to union
51 timestamp: The timestamp/version for this union operation
52
53 Returns:
54 True if union was performed, False if elements were already connected
55 """
56 # Find roots of both elements
57 root_a = self.find(a)
58 root_b = self.find(b)
59
60 # If already in the same set, no union needed
61 if root_a == root_b:
62 return False
63
64 # Union by rank - attach smaller rank tree under root of higher rank tree
65 if self.rank[root_a] > self.rank[root_b]:
66 # Make root_a the parent of root_b
67 self.version[root_b] = timestamp
68 self.parent[root_b] = root_a
69 else:
70 # Make root_b the parent of root_a
71 self.version[root_a] = timestamp
72 self.parent[root_a] = root_b
73 # If ranks were equal, increment the rank of the new root
74 if self.rank[root_a] == self.rank[root_b]:
75 self.rank[root_b] += 1
76
77 return True
78
79
80class DistanceLimitedPathsExist:
81 """
82 Data structure to answer queries about whether two nodes are connected
83 using only edges with distance less than a given limit.
84 """
85
86 def __init__(self, n: int, edgeList: List[List[int]]) -> None:
87 """
88 Initialize the data structure with nodes and edges.
89
90 Args:
91 n: Number of nodes
92 edgeList: List of edges, each edge is [node1, node2, distance]
93 """
94 # Create persistent union-find structure
95 self.persistent_union_find = PersistentUnionFind(n)
96
97 # Sort edges by distance (ascending order)
98 edgeList.sort(key=lambda edge: edge[2])
99
100 # Process edges in order of increasing distance
101 # Union nodes with the edge distance as timestamp
102 for u, v, distance in edgeList:
103 self.persistent_union_find.union(u, v, distance)
104
105 def query(self, p: int, q: int, limit: int) -> bool:
106 """
107 Check if nodes p and q are connected using only edges with distance < limit.
108
109 Args:
110 p: First node
111 q: Second node
112 limit: Distance limit for edges
113
114 Returns:
115 True if p and q are connected with edges having distance < limit
116 """
117 # Two nodes are connected if they have the same root
118 # when considering only edges with distance < limit
119 return self.persistent_union_find.find(p, limit) == self.persistent_union_find.find(q, limit)
120
1import java.util.Arrays;
2
3/**
4 * Persistent Union-Find data structure that maintains version history
5 * Allows querying the state of connected components at different timestamps
6 */
7class PersistentUnionFind {
8 private static final int INFINITY = 1 << 30; // Large value representing no version limit
9 private int[] rank; // Rank array for union by rank optimization
10 private int[] parent; // Parent array for tracking root of each element
11 private int[] version; // Version/timestamp when parent relationship was established
12
13 /**
14 * Initialize persistent union-find with n elements
15 * @param n Number of elements
16 */
17 public PersistentUnionFind(int n) {
18 rank = new int[n];
19 parent = new int[n];
20 version = new int[n];
21
22 // Initially, each element is its own parent with infinite version
23 for (int i = 0; i < n; i++) {
24 parent[i] = i;
25 version[i] = INFINITY;
26 }
27 }
28
29 /**
30 * Find root of element x at given timestamp t
31 * @param x Element to find root for
32 * @param t Timestamp to query at
33 * @return Root of element x at timestamp t
34 */
35 public int find(int x, int t) {
36 // If x is its own parent or the parent relationship was established after timestamp t
37 if (parent[x] == x || version[x] >= t) {
38 return x;
39 }
40 // Recursively find parent at timestamp t
41 return find(parent[x], t);
42 }
43
44 /**
45 * Union two elements at given timestamp
46 * @param a First element
47 * @param b Second element
48 * @param t Timestamp of union operation
49 * @return true if union was performed, false if already connected
50 */
51 public boolean union(int a, int b, int t) {
52 // Find current roots (using infinite timestamp to get latest state)
53 int rootA = find(a, INFINITY);
54 int rootB = find(b, INFINITY);
55
56 // Already in same component
57 if (rootA == rootB) {
58 return false;
59 }
60
61 // Union by rank: attach smaller rank tree under root of higher rank tree
62 if (rank[rootA] > rank[rootB]) {
63 version[rootB] = t; // Record when rootB became child of rootA
64 parent[rootB] = rootA;
65 } else {
66 version[rootA] = t; // Record when rootA became child of rootB
67 parent[rootA] = rootB;
68 // If ranks are equal, increment rank of new root
69 if (rank[rootA] == rank[rootB]) {
70 rank[rootB]++;
71 }
72 }
73 return true;
74 }
75}
76
77/**
78 * Data structure for answering connectivity queries with distance limit
79 * Checks if two nodes are connected using only edges with weight less than limit
80 */
81public class DistanceLimitedPathsExist {
82 private PersistentUnionFind persistentUnionFind;
83
84 /**
85 * Initialize with graph edges
86 * @param n Number of nodes in graph (0 to n-1)
87 * @param edgeList Array of edges where each edge is [from, to, weight]
88 */
89 public DistanceLimitedPathsExist(int n, int[][] edgeList) {
90 persistentUnionFind = new PersistentUnionFind(n);
91
92 // Sort edges by weight in ascending order
93 Arrays.sort(edgeList, (edge1, edge2) -> edge1[2] - edge2[2]);
94
95 // Process edges in order of increasing weight
96 // Use edge weight as timestamp for union operation
97 for (int[] edge : edgeList) {
98 int from = edge[0];
99 int to = edge[1];
100 int weight = edge[2];
101 persistentUnionFind.union(from, to, weight);
102 }
103 }
104
105 /**
106 * Query if two nodes are connected using only edges with weight < limit
107 * @param p First node
108 * @param q Second node
109 * @param limit Maximum edge weight allowed
110 * @return true if p and q are connected with edges having weight < limit
111 */
112 public boolean query(int p, int q, int limit) {
113 // Check if p and q have same root when considering only edges with weight < limit
114 return persistentUnionFind.find(p, limit) == persistentUnionFind.find(q, limit);
115 }
116}
117
118/**
119 * Your DistanceLimitedPathsExist object will be instantiated and called as such:
120 * DistanceLimitedPathsExist obj = new DistanceLimitedPathsExist(n, edgeList);
121 * boolean param_1 = obj.query(p,q,limit);
122 */
123
1class PersistentUnionFind {
2private:
3 vector<int> rank; // Rank for union by rank optimization
4 vector<int> parent; // Parent array for disjoint set
5 vector<int> version; // Timestamp when a node's parent was updated
6
7public:
8 /**
9 * Constructor: Initialize persistent union-find structure
10 * @param n: Number of nodes (0 to n-1)
11 */
12 PersistentUnionFind(int n)
13 : rank(n, 0)
14 , parent(n)
15 , version(n, INT_MAX) {
16 // Initially, each node is its own parent
17 for (int i = 0; i < n; i++) {
18 parent[i] = i;
19 }
20 }
21
22 /**
23 * Find the root of node x at time t
24 * Only follows parent pointers that were created before time t
25 * @param x: Node to find root for
26 * @param t: Time threshold
27 * @return: Root of node x considering only unions before time t
28 */
29 int find(int x, int t) {
30 // If x is its own parent or the parent link was created at/after time t
31 if (parent[x] == x || version[x] >= t) {
32 return x;
33 }
34 // Recursively find parent (no path compression to maintain persistence)
35 return find(parent[x], t);
36 }
37
38 /**
39 * Union two sets containing nodes a and b at time t
40 * @param a: First node
41 * @param b: Second node
42 * @param t: Timestamp of this union operation
43 * @return: true if union was performed, false if already in same set
44 */
45 bool unionSet(int a, int b, int t) {
46 // Find current roots (using INT_MAX to get latest state)
47 int rootA = find(a, INT_MAX);
48 int rootB = find(b, INT_MAX);
49
50 // Already in the same set
51 if (rootA == rootB) {
52 return false;
53 }
54
55 // Union by rank: attach smaller rank tree under root of higher rank tree
56 if (rank[rootA] > rank[rootB]) {
57 version[rootB] = t; // Record when rootB's parent changed
58 parent[rootB] = rootA;
59 } else {
60 version[rootA] = t; // Record when rootA's parent changed
61 parent[rootA] = rootB;
62 // If ranks are equal, increment rank of new root
63 if (rank[rootA] == rank[rootB]) {
64 rank[rootB]++;
65 }
66 }
67 return true;
68 }
69};
70
71class DistanceLimitedPathsExist {
72private:
73 PersistentUnionFind persistentUF; // Persistent union-find data structure
74
75public:
76 /**
77 * Constructor: Build the graph with distance-limited connectivity
78 * @param n: Number of nodes in the graph
79 * @param edgeList: List of edges where each edge is [u, v, distance]
80 */
81 DistanceLimitedPathsExist(int n, vector<vector<int>>& edgeList)
82 : persistentUF(n) {
83 // Sort edges by distance (ascending order)
84 sort(edgeList.begin(), edgeList.end(),
85 [](const vector<int>& a, const vector<int>& b) {
86 return a[2] < b[2];
87 });
88
89 // Process edges in order of increasing distance
90 // Use edge distance as the timestamp for union operations
91 for (const auto& edge : edgeList) {
92 persistentUF.unionSet(edge[0], edge[1], edge[2]);
93 }
94 }
95
96 /**
97 * Query if there exists a path between p and q using only edges with distance < limit
98 * @param p: Start node
99 * @param q: End node
100 * @param limit: Distance limit (exclusive)
101 * @return: true if p and q are connected using edges with distance < limit
102 */
103 bool query(int p, int q, int limit) {
104 // Check if p and q have the same root when considering only edges with distance < limit
105 return persistentUF.find(p, limit) == persistentUF.find(q, limit);
106 }
107};
108
109/**
110 * Your DistanceLimitedPathsExist object will be instantiated and called as such:
111 * DistanceLimitedPathsExist* obj = new DistanceLimitedPathsExist(n, edgeList);
112 * bool param_1 = obj->query(p,q,limit);
113 */
114
1// Global variables for the persistent union-find structure
2let rank: number[];
3let parent: number[];
4let version: number[];
5
6// Global variable for the main data structure
7let puf: {
8 find: (x: number, t: number) => number;
9 union: (a: number, b: number, t: number) => boolean;
10};
11
12/**
13 * Find the root of element x at time t
14 * Uses path compression with version checking
15 * @param x - The element to find the root for
16 * @param t - The time/version threshold
17 * @returns The root of element x at time t
18 */
19const find = (x: number, t: number): number => {
20 // If x is its own parent or the version is beyond threshold, return x
21 if (parent[x] === x || version[x] >= t) {
22 return x;
23 }
24 // Recursively find the parent at time t
25 return find(parent[x], t);
26};
27
28/**
29 * Union two elements at a specific time
30 * Uses union by rank optimization
31 * @param a - First element to union
32 * @param b - Second element to union
33 * @param t - The time/version when union occurs
34 * @returns True if union was performed, false if already connected
35 */
36const union = (a: number, b: number, t: number): boolean => {
37 // Find roots at current time (Infinity means latest version)
38 const rootA = find(a, Infinity);
39 const rootB = find(b, Infinity);
40
41 // Already in the same component
42 if (rootA === rootB) {
43 return false;
44 }
45
46 // Union by rank: attach smaller rank tree under root of higher rank tree
47 if (rank[rootA] > rank[rootB]) {
48 version[rootB] = t;
49 parent[rootB] = rootA;
50 } else {
51 version[rootA] = t;
52 parent[rootA] = rootB;
53 // If ranks are equal, increment rank of new root
54 if (rank[rootA] === rank[rootB]) {
55 rank[rootB]++;
56 }
57 }
58
59 return true;
60};
61
62/**
63 * Initialize the distance-limited paths data structure
64 * @param n - Number of nodes in the graph
65 * @param edgeList - List of edges [u, v, distance]
66 */
67const initDistanceLimitedPathsExist = (n: number, edgeList: number[][]): void => {
68 // Initialize persistent union-find arrays
69 rank = Array(n).fill(0);
70 parent = Array.from({ length: n }, (_, index) => index);
71 version = Array(n).fill(Infinity);
72
73 // Create persistent union-find object
74 puf = { find, union };
75
76 // Sort edges by distance in ascending order
77 edgeList.sort((edgeA, edgeB) => edgeA[2] - edgeB[2]);
78
79 // Process each edge and union nodes with edge distance as version
80 for (const [nodeU, nodeV, distance] of edgeList) {
81 puf.union(nodeU, nodeV, distance);
82 }
83};
84
85/**
86 * Query if two nodes are connected with edges having distance less than limit
87 * @param p - First node
88 * @param q - Second node
89 * @param limit - Maximum allowed edge distance
90 * @returns True if nodes are connected with edges under the limit
91 */
92const query = (p: number, q: number, limit: number): boolean => {
93 // Check if both nodes have the same root at the given limit time
94 return puf.find(p, limit) === puf.find(q, limit);
95};
96
97/**
98 * Class wrapper for Distance Limited Paths Exist problem
99 */
100class DistanceLimitedPathsExist {
101 constructor(n: number, edgeList: number[][]) {
102 initDistanceLimitedPathsExist(n, edgeList);
103 }
104
105 query(p: number, q: number, limit: number): boolean {
106 return query(p, q, limit);
107 }
108}
109
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):- Creating the PersistentUnionFind takes
O(n)
time to initialize arrays - Sorting the edgeList takes
O(m log m)
time wherem
is the number of edges - Performing union operations for all edges takes
O(m × α(n))
time, whereα(n)
is the inverse Ackermann function (practically constant) - Total initialization time:
O(n + m log m + m × α(n))
=O(n + m log m)
- Creating the PersistentUnionFind takes
-
Query operation:
- Each query calls
find
twice with a time limit parameter - The
find
operation with time limit traverses the parent chain until it finds a root or a version that exceeds the time limit - In the worst case, this takes
O(n)
time per find operation (when the tree forms a long chain) - Per query time complexity:
O(n)
- Each query calls
Space Complexity:
-
PersistentUnionFind class:
rank
array:O(n)
spacep
(parent) array:O(n)
spaceversion
array:O(n)
space- Total:
O(n)
space
-
DistanceLimitedPathsExist class:
- Stores a reference to PersistentUnionFind:
O(n)
space - The sorted edgeList is created temporarily during initialization but not stored
- Total space:
O(n)
- Stores a reference to PersistentUnionFind:
-
Overall space complexity:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Multiple Edges Between Same Nodes
The Pitfall: The graph may contain multiple edges between the same pair of nodes with different distances. A common mistake is assuming that we should only keep the edge with minimum distance between any two nodes, leading to incorrect preprocessing like:
# WRONG: Deduplicating edges by keeping only minimum distance
edge_map = {}
for u, v, dis in edgeList:
key = (min(u,v), max(u,v))
if key not in edge_map or edge_map[key] > dis:
edge_map[key] = dis
edgeList = [[u, v, d] for (u,v), d in edge_map.items()]
Why It's Wrong:
While it seems logical to only keep the minimum distance edge, the Persistent Union-Find actually handles multiple edges correctly by processing them in sorted order. When we union nodes that are already connected, the union
method returns False
and doesn't modify the structure. The version timestamp of their first connection is preserved, which is exactly what we want.
The Solution: Simply process all edges as given without deduplication:
def __init__(self, n: int, edgeList: List[List[int]]) -> None:
self.persistent_union_find = PersistentUnionFind(n)
edgeList.sort(key=lambda edge: edge[2]) # Just sort, no deduplication
for u, v, distance in edgeList:
self.persistent_union_find.union(u, v, distance)
2. Using Wrong Comparison in Query (< vs <=)
The Pitfall:
Misunderstanding the problem requirement and using <=
instead of <
for the limit comparison:
# WRONG: Including edges with distance equal to limit
def query(self, p: int, q: int, limit: int) -> bool:
return self.persistent_union_find.find(p, limit + 1) == self.persistent_union_find.find(q, limit + 1)
Why It's Wrong:
The problem specifically asks for edges with distance strictly less than the limit. Using limit + 1
or any adjustment that includes edges with distance equal to limit
will produce incorrect results.
The Solution: Use the limit directly as the timestamp in the find operation:
def query(self, p: int, q: int, limit: int) -> bool:
return self.persistent_union_find.find(p, limit) == self.persistent_union_find.find(q, limit)
3. Path Compression Breaking Persistence
The Pitfall: Implementing standard path compression in the find operation, which modifies the parent pointers:
# WRONG: Path compression destroys version history
def find(self, x: int, timestamp: float = inf) -> int:
if self.parent[x] != x and self.version[x] < timestamp:
self.parent[x] = self.find(self.parent[x], timestamp) # Path compression
return self.parent[x]
Why It's Wrong: Path compression modifies the tree structure by making nodes point directly to the root. This destroys the version history we carefully maintained. Future queries with different timestamps would get incorrect results because the intermediate parent relationships have been lost.
The Solution: Never modify the parent pointers during find operations. Accept the slightly higher time complexity in exchange for correctness:
def find(self, x: int, timestamp: float = inf) -> int:
if self.parent[x] == x or self.version[x] >= timestamp:
return x
return self.find(self.parent[x], timestamp) # No path compression
4. Forgetting to Handle Disconnected Components
The Pitfall: Assuming all nodes will eventually be connected and not properly initializing isolated nodes:
# WRONG: Not initializing version array properly
def __init__(self, n: int) -> None:
self.version: List[float] = [0] * n # Wrong initial value
Why It's Wrong: Nodes that never get connected to anything should never match with other components at any timestamp. Using 0 or any finite value as initial version could cause incorrect matches in queries.
The Solution: Initialize version array with infinity to ensure unconnected nodes stay independent:
def __init__(self, n: int) -> None:
self.version: List[float] = [inf] * n # Correct initialization
Which data structure is used to implement priority queue?
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
Minimum Spanning Tree Forests The Umbristan Department of Forestry UDF is tackling a rather difficult problem and the Umbristan government has assigned you as one of its best workers to go resolve the issue When you arrive you are quickly briefed on the problem at hand Inside the Umbristan National Park there exists an
Want a Structured Path to Master System Design Too? Don’t Miss This!