Checking Existence of Edge Length Limited Paths

Updated on 20 May, 2025
Checking Existence of Edge Length Limited Paths header image

Problem Statement

This problem involves working with an undirected graph defined by n nodes using an edgeList. Each item in edgeList consists of three elements, [ui, vi, disi], where ui and vi are node identifiers indicating an edge between them and disi represents the distance of this edge. It is important to note that there can be multiple edges between the same two nodes, each potentially having different distances.

Given a list of queries, each query [pj, qj, limitj], the task is to determine if there exists at least one path between nodes pj and qj such that every edge on this path has a distance less than limitj.

The solution should produce a boolean array answer with each element corresponding to the result of a query, where true is returned if the conditions are met for that respective query and false otherwise.

Examples

Example 1

Input:

n = 3, edgeList = [[0,1,2],[1,2,4],[2,0,8],[1,0,16]], queries = [[0,1,2],[0,2,5]]

Output:

[false,true]

Explanation:

The above figure shows the given graph. Note that there are two overlapping edges between 0 and 1 with distances 2 and 16.
For the first query, between 0 and 1 there is no path where each distance is less than 2, thus we return false for this query.
For the second query, there is a path (0 -> 1 -> 2) of two edges with distances less than 5, thus we return true for this query.

Example 2

Input:

n = 5, edgeList = [[0,1,10],[1,2,5],[2,3,9],[3,4,13]], queries = [[0,4,14],[1,4,13]]

Output:

[true,false]

Explanation:

The above figure shows the given graph.

Constraints

  • 2 <= n <= 105
  • 1 <= edgeList.length, queries.length <= 105
  • edgeList[i].length == 3
  • queries[j].length == 3
  • 0 <= ui, vi, pj, qj <= n - 1
  • ui != vi
  • pj != qj
  • 1 <= disi, limitj <= 109
  • There may be multiple edges between two nodes.

Approach and Intuition

To solve this problem effectively, we must focus on both the graph's connectivity and the constraints on distances given by each query. Here's a breakdown of the steps involved:

  1. Representation and Preprocessing

    • Construct an adjacency list representation of the graph using the edgeList. During this process, ensure to filter and store only those edges which might be relevant, i.e., those whose distance might be less than any limitj in the queries.
  2. Pathfinding with Constraints

    • For each query, the task is to check connectivity under constraints:
      • You could use a modified Breadth-First Search (BFS) or Depth-First Search (DFS) where you only follow edges that satisfy the distance condition (< limitj).
      • An alternative and often efficient method in such 'constraint' problems is to sort both the edges and the queries by their respective distances and limits. This can leverage Union-Find (or Disjoint Set Union, DSU) structures for dynamic connectivity checks:
        • As you progress through sorted edges, you connect nodes and simultaneously handle the queries that have a limit less than the current edge's distance.
  3. Efficiency Considerations

    • Sorting edges and queries might seem costly, but both operations are feasible within given constraints since they will operate at O(m log m) and O(q log q) complexities, where m is the number of edges and q is the number of queries.
    • The DSU operations (union and find) are very efficient with path compression and will generally operate near constant time for our input size.

This approach is structured to exploit sorted processing and DSU for efficient query handling without repetitively searching through the graph, thus aiming for a balance between preprocessing time and query response efficiency. This supports real-time querying even in scenarios of dense graphs and numerous queries, maintaining adherence to provided constraints.

Solutions

  • C++
  • Java
  • JavaScript
  • Python
cpp
class DisjointSet {
public:
    vector<int> parent;
    vector<int> size;

    DisjointSet(int count) {
        parent = vector<int>(count);
        size = vector<int>(count, 0);
        for (int i = 0; i < count; ++i) {
            parent[i] = i;
        }
    }

    int find(int element) {
        if (parent[element] != element) {
            parent[element] = find(parent[element]);
        }
        return parent[element];
    }

    void unify(int elem1, int elem2) {
        int root1 = find(elem1);
        int root2 = find(elem2);

        if (root1 == root2) return;

        if (size[root1] > size[root2]) {
            parent[root2] = root1;
        } else if (size[root1] < size[root2]) {
            parent[root1] = root2;
        } else {
            parent[root1] = root2;
            size[root2]++;
        }
    }

    bool isConnected(int elem1, int elem2) {
        return find(elem1) == find(elem2);
    }
};


class Solution {
public:
    static bool edgeComparer(vector<int>& edge1, vector<int>& edge2) {
        return edge1[2] < edge2[2];
    }

    vector<bool> distanceLimitedPathsExist(int n, vector<vector<int>>& edgeList, vector<vector<int>>& queries) {
        DisjointSet ds(n);
        int queryLen = queries.size();
        vector<bool> results(queryLen);

        vector<vector<int>> indexedQueries(queryLen);
        for (int i = 0; i < queryLen; ++i) {
            indexedQueries[i] = queries[i];
            indexedQueries[i].push_back(i);
        }

        int currentEdge = 0;

        sort(edgeList.begin(), edgeList.end(), edgeComparer);
        sort(indexedQueries.begin(), indexedQueries.end(), edgeComparer);

        for (auto& query : indexedQueries) {
            int start = query[0];
            int end = query[1];
            int limit = query[2];
            int originalIndex = query[3];

            while (currentEdge < edgeList.size() && edgeList[currentEdge][2] < limit) {
                ds.unify(edgeList[currentEdge][0], edgeList[currentEdge][1]);
                currentEdge++;
            }

            results[originalIndex] = ds.isConnected(start, end);
        }

        return results;
    }
};

The provided C++ code defines a solution for checking the existence of edge-length limited paths using Disjoint Set Union (DSU) or Union-Find data structure. This approach efficiently manages the connectivity queries and the updates within an undirected graph.

Breakdown of the Code:

  • Class DisjointSet:

    • Initializes and manages a disjoint set:
      • parent: Tracks the representative (or parent) of each element.
      • size: Helps in balancing the trees during unification by keeping track of the size of each tree.
    • Includes essential methods like find, unify, and isConnected:
      • find: Implements path compression to find the representative of an element efficiently.
      • unify: Joins two subsets into a single subset while maintaining a relatively balanced structure using the size array.
      • isConnected: Checks if two elements are in the same subset by comparing their representatives.
  • Class Solution:

    • Implements the method distanceLimitedPathsExist to determine if a path exists between two nodes with an edge weight less than a given limit for a set of queries:
      • Sorts the edge list and queries based on the edge weight and query limits to process them efficiently in increasing order of their weights.
      • Utilizes a modified merging technique:
        • For each query, it unifies nodes connected by edges that have weights less than the limit specified in the query.
        • After processing the relevant edges for a query, it checks if there is a path (i.e., if the nodes are connected in the disjoint set) that satisfies the distance limit.

Conclusion:

This approach is particularly efficient for scenarios with multiple connectivity queries on potentially large graphs where dynamic changes to the graph structure are limited to unification operations. The sorting of edges and queries ensures that each union or find operation is needed only once per query/edge, leading to significant optimizations in scenarios with large inputs. This methodology effectively combines union-find with sorting to manage and solve the edge-length limited paths problem within reasonable time and space complexity.

java
class DisjointSet {
    private int[] parent;
    private int[] height;

    DisjointSet(int n) {
        parent = new int[n];
        height = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    public int find(int element) {
        if (parent[element] != element) {
            parent[element] = find(parent[element]);
        }
        return parent[element];
    }

    public void union(int elem1, int elem2) {
        int root1 = find(elem1);
        int root2 = find(elem2);

        if (root1 == root2) return;

        if (height[root1] > height[root2]) {
            parent[root2] = root1;
        } else if (height[root1] < height[root2]) {
            parent[root1] = root2;
        } else {
            parent[root1] = root2;
            height[root2]++;
        }
    }

    public boolean isConnected(int elem1, int elem2) {
        return find(elem1) == find(elem2);
    }
};

class Solution {
    private void sortByWeight(int[][] edges) {
        Arrays.sort(edges, (a, b) -> Integer.compare(a[2], b[2]));
    }
    
    public boolean[] canReach(int n, int[][] edgeList, int[][] queries) {
        DisjointSet ds = new DisjointSet(n);
        int queryCount = queries.length;
        boolean[] results = new boolean[queryCount];
        int[][] indexedQueries = new int[queryCount][4];
        for (int i = 0; i < queryCount; i++) {
            indexedQueries[i][0] = queries[i][0];
            indexedQueries[i][1] = queries[i][1];
            indexedQueries[i][2] = queries[i][2];
            indexedQueries[i][3] = i;
        }
        
        sortByWeight(edgeList);
        sortByWeight(indexedQueries);

        int edgePos = 0;
        for (int[] query : indexedQueries) {
            int src = query[0];
            int dest = query[1];
            int limit = query[2];
            int idx = query[3];

            while (edgePos < edgeList.length && edgeList[edgePos][2] < limit) {
                ds.union(edgeList[edgePos][0], edgeList[edgePos][1]);
                edgePos++;
            }

            results[idx] = ds.isConnected(src, dest);
        }
        
        return results;
    }
}

The Java solution provided addresses the problem of determining whether certain paths exist between nodes in a graph, given constraints on edge lengths. This solution uses a Disjoint Set (or Union-Find) data structure, enhanced with path compression and union by rank to efficiently handle the union and find operations needed to assess connectivity between node pairs under the given conditions.

Key Components:

  • DisjointSet Class:

    • Manages the union-find operations with methods find, union, and isConnected.
    • Uses two arrays, parent and height, to keep track of the connected components and the height of each component's tree, ensuring efficient unions and finds.
  • Solution Class:

    • Contains the canReach method to determine if specific queries can be fulfilled.
    • Processes edges and queries by sorting them according to their weight, to handle them in an increasing order of constraints.

Procedure:

  1. Instantiate the DisjointSet.
  2. Sort the edgeList and queries based on edge and query limits.
  3. Loop through each query, processing edges that do not exceed the current query's limit. This ensures that only relevant edges affect the connectivity status for each query.
  4. For each query, use isConnected to determine if the source and destination nodes are in the same connected component under the current constraints.
  5. Return the results as a boolean array with each element corresponding to the original order of the queries indicating if the connection between nodes for each respective query is possible.

This solution effectively utilizes the union-find structure to dynamically manage connectivity as edges are processed in increasing order of weights, ensuring that each query is answered accurately according to the limitations imposed by edge lengths.

js
class DisjointSet {
    constructor(capacity) {
        this.parent = [];
        this.rank = [];
        for (let i = 0; i < capacity; ++i) {
            this.parent[i] = i;
        }
    }

    find(element) {
        if (this.parent[element] !== element) {
            this.parent[element] = this.find(this.parent[element]);
        }
        return this.parent[element];
    }

    unite(el1, el2) {
        let root1 = this.find(el1);
        let root2 = this.find(el2);
        
        // If they have the same root, they are already united
        if (root1 === root2) {
            return;
        }

        if (this.rank[root1] > this.rank[root2]) {
            this.parent[root2] = root1;
        } else if (this.rank[root1] < this.rank[root2]) {
            this.parent[root1] = root2;
        } else {
            this.parent[root1] = root2;
            this.rank[root2]++;
        }
    }
    
    isConnected(el1, el2) {
        return this.find(el1) === this.find(el2);
    }
}

var hasPathWithLimits = function(numNodes, edges, queries) {
    let ds = new DisjointSet(numNodes);
    let queriesCount = queries.length;
    let result = new Array(queriesCount);

    let indexedQueries = [];
    for (let i = 0; i < queriesCount; ++i) {
        indexedQueries.push([...queries[i], i]);
    }

    edges.sort((a, b) => a[2] - b[2]);
    indexedQueries.sort((a, b) => a[2] - b[2]);

    let edgePos = 0;

    indexedQueries.forEach(([node1, node2, maxDistance, originalIdx]) => {
        while (edgePos < edges.length && edges[edgePos][2] < maxDistance) {
            let [n1, n2] = edges[edgePos];
            ds.unite(n1, n2);
            edgePos++;
        }

        result[originalIdx] = ds.isConnected(node1, node2);
    });

    return result;
};

This function, hasPathWithLimits, determines if a path between two nodes exists in a graph, where the path length limitations are specified for each query. The solution employs a Disjoint Set (or Union-Find) data structure, which efficiently manages a partition of a set into disjoint (non-overlapping) subsets.

Here's how the provided solution works using JavaScript:

  • A DisjointSet class is defined to manage union and find operations:

    • constructor initializes parent reference and rank for each node.
    • find retrieves the root parent for a node, employing path compression.
    • unite joins two subsets into a single subset, utilizing union by rank.
    • isConnected checks if two elements are in the same subset.
  • The main function hasPathWithLimits accepts:

    • numNodes: Number of nodes in the graph.
    • edges: An array of edges where each edge is denoted as [node1, node2, weight].
    • queries: An array where each query is specified as [node1, node2, maxDistance].
  • Execution steps:

    1. An instance of DisjointSet is created.
    2. The queries are extended with their index and sorted based on the maxDistance.
    3. The edges are sorted by their weight.
    4. Iterate through each query:
      • Continue to unite nodes from the sorted edges list until the edge weight is less than maxDistance from the query.
      • Check if the two nodes in the current query are connected in the disjoint set.
      • Store the result in the corresponding index of the result array.
  • The sorted approach on edges and queries ensures that the function only considers relevant edges for each query, which makes the method efficient in handling large input data sets.

This function returns an array where each element reflects whether the respective query can be satisfied given the constraints on the path lengths.

python
class DisjointSet:
    def __init__(self, size: int):
        self.parent = [0] * size
        self.rank = [0] * size
        for i in range(size):
            self.parent[i] = i

    def locate(self, element: int) -> int:
        if self.parent[element] != element:
            self.parent[element] = self.locate(self.parent[element])
        return self.parent[element]

    def union(self, elem1: int, elem2: int):
        root1 = self.locate(elem1)
        root2 = self.locate(elem2)

        if root1 == root2:
            return

        if self.rank[root1] > self.rank[root2]:
            self.parent[root2] = root1
        elif self.rank[root1] < self.rank[root2]:
            self.parent[root1] = root2
        else:
            self.parent[root2] = root1
            self.rank[root1] += 1

    def connected(self, elem1: int, elem2: int) -> bool:
        return self.locate(elem1) == self.locate(elem2)

class Solution:
    def distanceLimitedPathsExist(self, n: int, edge_list: list, queries: list) -> list:
        ds = DisjointSet(n)
        results = [False] * len(queries)
        index_queries = [[*query, i] for i, query in enumerate(queries)]

        edge_list.sort(key=lambda x: x[2])
        index_queries.sort(key=lambda x: x[2])

        index = 0
        for u, v, lim, idx in index_queries:
            while index < len(edge_list) and edge_list[index][2] < lim:
                a, b = edge_list[index][:2]
                ds.union(a, b)
                index += 1
            results[idx] = ds.connected(u, v)

        return results

The provided Python 3 solution addresses the problem of determining whether certain paths exist between nodes in an undirected graph, adhering to specified edge length limitations. The solution uses a Disjoint Set (or Union-Find) data structure to efficiently manage and query the connectivity of nodes. Here’s how the solution is structured:

  • DisjointSet Class:

    • Initialization: Creates parent and rank arrays sized according to the number of nodes. Each node is initially its own parent, indicating that it stands alone.
    • Locate Function: Implements path compression to find the root of a set for a given element, which speeds up future queries.
    • Union Function: Connects two elements, using rank to keep the tree flat and thus reduce the time complexity of future operations.
    • Connected Function: Determines if two elements are in the same set by checking if they have the same root.
  • Solution Class:

    • Method distanceLimitedPathsExist: Takes the number of nodes, a list of edges and a list of queries. Each query consists of two nodes and the limit on the maximum path weight considered for connectivity.
      • Initializes an instance of DisjointSet.
      • Processes edges and queries based on their weights using two sorted lists.
      • For each query checks connectivity between the specified nodes using the DisjointSet instance, as long as the edge weights are less than the specified limit in the query.

Execution Flow:

  1. Sort both the edge list and the queries by weight/limit.
  2. Use a two-pointer technique to avoid re-scanning the entire edge list for each query.
  3. For each query, iterate through edges that do not exceed the query's limit and union nodes connected by those edges.
  4. Determine if the nodes in each query are connected.

The result is a list indicating for each query whether a path exists between the nodes under the given constraints. This method ensures efficient processing of large graphs and numerous queries by leveraging the DisjointSet operations, which operate in nearly constant time due to path compression and rank-based union operations.

Comments

No comments yet.