Facebook Pixel

3532. Path Existence Queries in a Graph I

Problem Description

You have a graph with n nodes labeled from 0 to n - 1. You're given:

  • An integer array nums of length n that is sorted in non-decreasing order
  • An integer maxDiff that determines which nodes can be connected

The graph's edges are formed based on a simple rule: an undirected edge exists between nodes i and j if and only if the absolute difference between their corresponding values in nums is at most maxDiff. In other words, nodes i and j are connected if |nums[i] - nums[j]| <= maxDiff.

You're also given a 2D array queries where each query queries[i] = [ui, vi] asks whether there exists a path between nodes ui and vi in the graph.

Your task is to return a boolean array answer where answer[i] is true if there's a path between nodes ui and vi for the i-th query, and false otherwise.

The key insight here is that since nums is sorted in non-decreasing order, nodes can only form connected components with consecutive indices. If the difference between consecutive elements in nums exceeds maxDiff, those nodes will be in different connected components. The solution cleverly tracks which connected component each node belongs to by checking gaps in the sorted array - whenever nums[i] - nums[i-1] > maxDiff, a new connected component starts. Then, for each query, it simply checks if both nodes belong to the same connected component.

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

Intuition

Let's think about what edges can exist in this graph. Since nums is sorted, we have nums[0] <= nums[1] <= nums[2] <= ... <= nums[n-1].

For any two nodes i and j where i < j, we know that nums[j] >= nums[i], so the absolute difference is simply nums[j] - nums[i].

Now, here's the crucial observation: if node i can connect to node j (where i < j), then node i can also connect to all nodes between i and j. Why? Because for any node k where i < k < j, we have nums[i] <= nums[k] <= nums[j], which means nums[k] - nums[i] <= nums[j] - nums[i] <= maxDiff.

This means that connected components must consist of consecutive indices. A connected component can't "skip" nodes - if nodes i and j are in the same component, then all nodes between them must also be in that component.

So when does a connected component end and a new one begin? It happens when we find a gap that's too large. Specifically, if we're looking at consecutive nodes i-1 and i, and nums[i] - nums[i-1] > maxDiff, then node i cannot connect to node i-1. Since connected components must be consecutive, this means node i starts a new connected component.

This leads us to a simple solution: we can scan through the array once and mark which connected component each node belongs to. We start with component 0, and whenever we find a gap larger than maxDiff between consecutive elements, we increment the component counter. Then, to check if two nodes are connected, we just check if they belong to the same component - a constant time operation!

Learn more about Union Find, Graph and Binary Search patterns.

Solution Approach

The implementation uses a grouping strategy to efficiently determine connectivity between nodes.

Data Structure:

  • Array g of size n: Stores the connected component index for each node
  • Variable cnt: Tracks the current connected component index, starting from 0

Algorithm Steps:

  1. Initialize the grouping array: Create an array g where g[i] will store which connected component node i belongs to. Set g[0] = 0 since the first node belongs to component 0.

  2. Identify connected components: Iterate through the sorted nums array from index 1 to n-1:

    • For each node i, check if nums[i] - nums[i-1] > maxDiff
    • If true, this indicates a "break" in connectivity - node i cannot connect to node i-1
    • When a break is found, increment cnt to start a new connected component
    • Assign g[i] = cnt to mark which component node i belongs to
  3. Process queries: For each query [u, v]:

    • Simply check if g[u] == g[v]
    • If they're equal, nodes u and v are in the same connected component, so return true
    • Otherwise, return false

Example walkthrough: If nums = [1, 3, 4, 8, 9] and maxDiff = 2:

  • Node 0 (value 1): g[0] = 0 (component 0)
  • Node 1 (value 3): 3 - 1 = 2 <= 2, so g[1] = 0 (same component)
  • Node 2 (value 4): 4 - 3 = 1 <= 2, so g[2] = 0 (same component)
  • Node 3 (value 8): 8 - 4 = 4 > 2, so increment cnt to 1, g[3] = 1 (new component)
  • Node 4 (value 9): 9 - 8 = 1 <= 2, so g[4] = 1 (same component as node 3)

Result: g = [0, 0, 0, 1, 1], meaning nodes 0, 1, 2 form one component and nodes 3, 4 form another.

Time Complexity: O(n + q) where q is the number of queries Space Complexity: O(n) for the grouping 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 concrete example to understand how the solution works.

Given:

  • nums = [2, 5, 6, 10, 15]
  • maxDiff = 3
  • queries = [[0, 2], [1, 4], [3, 4]]

Step 1: Build the connected components array g

We'll track which component each node belongs to by checking gaps between consecutive elements:

  • Node 0 (value 2): First node always starts component 0

    • g[0] = 0, cnt = 0
  • Node 1 (value 5): Check gap from previous

    • Gap: 5 - 2 = 3 ≤ 3 ✓ (within maxDiff)
    • g[1] = 0 (same component)
  • Node 2 (value 6): Check gap from previous

    • Gap: 6 - 5 = 1 ≤ 3 ✓ (within maxDiff)
    • g[2] = 0 (same component)
  • Node 3 (value 10): Check gap from previous

    • Gap: 10 - 6 = 4 > 3 ✗ (exceeds maxDiff)
    • Start new component: cnt = 1
    • g[3] = 1 (new component)
  • Node 4 (value 15): Check gap from previous

    • Gap: 15 - 10 = 5 > 3 ✗ (exceeds maxDiff)
    • Start new component: cnt = 2
    • g[4] = 2 (new component)

Result: g = [0, 0, 0, 1, 2]

This means:

  • Component 0: nodes {0, 1, 2} with values {2, 5, 6}
  • Component 1: node {3} with value {10}
  • Component 2: node {4} with value {15}

Step 2: Process queries

Now we can answer each query in O(1) time:

  • Query [0, 2]: Check if g[0] == g[2]

    • g[0] = 0 and g[2] = 0 → Same component → true
  • Query [1, 4]: Check if g[1] == g[4]

    • g[1] = 0 and g[4] = 2 → Different components → false
  • Query [3, 4]: Check if g[3] == g[4]

    • g[3] = 1 and g[4] = 2 → Different components → false

Final Answer: [true, false, false]

The beauty of this approach is that we only need one pass through the array to identify all connected components, then each query is answered by a simple array lookup comparison.

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def pathExistenceQueries(
6        self, n: int, nums: List[int], maxDiff: int, queries: List[List[int]]
7    ) -> List[bool]:
8        """
9        Determines if paths exist between pairs of indices based on a maximum difference constraint.
10      
11        Args:
12            n: The number of elements
13            nums: Array of values at each position
14            maxDiff: Maximum allowed difference between consecutive elements
15            queries: List of [u, v] pairs to check path existence
16          
17        Returns:
18            List of boolean values indicating if path exists for each query
19        """
20        # Initialize group/component array for each position
21        # This tracks which connected component each index belongs to
22        group_id = [0] * n
23      
24        # Counter for number of "breaks" in the path
25        # A break occurs when consecutive elements differ by more than maxDiff
26        current_group = 0
27      
28        # Iterate through consecutive pairs to identify breaks
29        for i in range(1, n):
30            # Check if difference between consecutive elements exceeds maxDiff
31            if nums[i] - nums[i - 1] > maxDiff:
32                # Increment group counter when a break is found
33                current_group += 1
34          
35            # Assign current group ID to this position
36            group_id[i] = current_group
37      
38        # For each query [u, v], check if both indices are in the same group
39        # If they share the same group ID, a valid path exists between them
40        result = [group_id[u] == group_id[v] for u, v in queries]
41      
42        return result
43
1class Solution {
2    /**
3     * Determines whether paths exist between pairs of nodes based on a maximum difference constraint.
4     * Two nodes can be in the same connected component if all consecutive differences in the path
5     * between them do not exceed maxDiff.
6     * 
7     * @param n       The number of nodes
8     * @param nums    Array of values associated with each node (assumed to be sorted)
9     * @param maxDiff Maximum allowed difference between consecutive nodes in a valid path
10     * @param queries Array of query pairs [u, v] to check if path exists between nodes u and v
11     * @return Array of boolean values indicating whether path exists for each query
12     */
13    public boolean[] pathExistenceQueries(int n, int[] nums, int maxDiff, int[][] queries) {
14        // Array to store the component ID for each node
15        // Nodes belong to the same component if they can reach each other
16        int[] componentIds = new int[n];
17        int currentComponentId = 0;
18      
19        // Build component IDs by checking consecutive differences
20        // When difference exceeds maxDiff, we start a new component
21        for (int i = 1; i < n; ++i) {
22            // Check if the difference between consecutive elements exceeds the threshold
23            if (nums[i] - nums[i - 1] > maxDiff) {
24                // Start a new component when gap is too large
25                currentComponentId++;
26            }
27            // Assign current component ID to this node
28            componentIds[i] = currentComponentId;
29        }
30
31        // Process each query
32        int numberOfQueries = queries.length;
33        boolean[] results = new boolean[numberOfQueries];
34      
35        for (int i = 0; i < numberOfQueries; ++i) {
36            int sourceNode = queries[i][0];
37            int targetNode = queries[i][1];
38          
39            // Path exists if both nodes belong to the same component
40            results[i] = componentIds[sourceNode] == componentIds[targetNode];
41        }
42      
43        return results;
44    }
45}
46
1class Solution {
2public:
3    vector<bool> pathExistenceQueries(int n, vector<int>& nums, int maxDiff, vector<vector<int>>& queries) {
4        // Create a group/component array to track connected components
5        // Each element stores the component ID for that index
6        vector<int> componentId(n);
7      
8        // Initialize component counter
9        int componentCount = 0;
10      
11        // Iterate through the array to identify component boundaries
12        // A new component starts when the difference between consecutive elements exceeds maxDiff
13        for (int i = 1; i < n; ++i) {
14            // Check if the difference between current and previous element exceeds maxDiff
15            if (nums[i] - nums[i - 1] > maxDiff) {
16                // Increment component count when we find a gap larger than maxDiff
17                ++componentCount;
18            }
19            // Assign the current component ID to this position
20            componentId[i] = componentCount;
21        }
22
23        // Process queries and build result vector
24        vector<bool> result;
25      
26        // For each query, check if both indices belong to the same component
27        for (const auto& query : queries) {
28            int startIndex = query[0];
29            int endIndex = query[1];
30          
31            // Path exists if both indices are in the same component
32            result.push_back(componentId[startIndex] == componentId[endIndex]);
33        }
34      
35        return result;
36    }
37};
38
1/**
2 * Determines if a path exists between pairs of indices based on maximum difference constraint.
3 * A path exists between two indices if all consecutive elements in the range have differences <= maxDiff.
4 * 
5 * @param n - The length of the nums array
6 * @param nums - The input array of numbers
7 * @param maxDiff - The maximum allowed difference between consecutive elements
8 * @param queries - Array of query pairs [u, v] to check path existence
9 * @returns Array of booleans indicating whether path exists for each query
10 */
11function pathExistenceQueries(
12    n: number,
13    nums: number[],
14    maxDiff: number,
15    queries: number[][],
16): boolean[] {
17    // Array to track group IDs for each index
18    // Elements in the same group can reach each other
19    const groupIds: number[] = Array(n).fill(0);
20  
21    // Counter for number of groups (incremented when gap > maxDiff found)
22    let groupCounter: number = 0;
23
24    // Build group IDs by checking consecutive element differences
25    for (let i = 1; i < n; ++i) {
26        // If difference exceeds maxDiff, start a new group
27        if (nums[i] - nums[i - 1] > maxDiff) {
28            ++groupCounter;
29        }
30        // Assign current group ID to this index
31        groupIds[i] = groupCounter;
32    }
33
34    // For each query, check if both indices belong to the same group
35    return queries.map(([startIndex, endIndex]: number[]) => 
36        groupIds[startIndex] === groupIds[endIndex]
37    );
38}
39

Time and Space Complexity

The time complexity of this algorithm is O(n + q), where n is the length of the nums array and q is the number of queries.

  • The first loop iterates through the array once from index 1 to n-1, performing constant-time operations (comparison and assignment) at each step, which takes O(n) time.
  • The list comprehension for processing queries iterates through all queries and performs constant-time array lookups and comparisons for each query, which takes O(q) time.
  • Overall time complexity: O(n + q)

The space complexity is O(n), where n is the length of the nums array.

  • The array g of size n is created to store the cumulative count values, requiring O(n) space.
  • The variable cnt uses constant space O(1).
  • The output list storing boolean results has length equal to the number of queries, but this is typically considered part of the output rather than auxiliary space.
  • Overall space complexity: O(n)

Note: The reference answer states O(n) for time complexity, which appears to assume that the number of queries q is bounded by n or not considered as a separate parameter in the complexity analysis.

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

Common Pitfalls

1. Misunderstanding the Graph Construction

A common misconception is thinking that the graph edges are only between consecutive indices in the sorted array. The problem states that an edge exists between ANY two nodes i and j where |nums[i] - nums[j]| <= maxDiff, not just consecutive ones.

Why the solution still works: Even though edges can exist between non-consecutive nodes, the sorted nature of nums creates a transitive property. If node i can connect to node j, and node j can connect to node k, then all three belong to the same component. The gap-checking approach correctly identifies when this transitivity breaks.

2. Incorrect Handling of Edge Cases

Developers might forget to handle:

  • Single element array (n = 1)
  • All elements being identical
  • maxDiff = 0 (only identical values can connect)

Solution: The current implementation handles these cases correctly:

# For n = 1, the loop doesn't execute, g[0] = 0
# For identical elements, nums[i] - nums[i-1] = 0 <= maxDiff always
# For maxDiff = 0, only consecutive identical values stay in same component

3. Off-by-One Errors in Component Assignment

A subtle bug could occur if incrementing the component counter at the wrong time:

Incorrect approach:

# Wrong: Incrementing before assignment
for i in range(1, n):
    if nums[i] - nums[i - 1] > maxDiff:
        current_group += 1
        group_id[i] = current_group  # Bug: should assign after increment check
    else:
        group_id[i] = current_group

Correct approach (as in solution):

for i in range(1, n):
    if nums[i] - nums[i - 1] > maxDiff:
        current_group += 1
    group_id[i] = current_group  # Assigns correctly whether incremented or not

4. Assuming Unsorted Input

If someone mistakenly assumes nums might be unsorted and tries to sort it first:

Wrong:

sorted_nums = sorted(nums)  # Don't do this!
# This breaks the index correspondence between nodes and their values

The problem explicitly states nums is already sorted, and the indices in queries refer to the original positions in nums.

5. Using Union-Find When Unnecessary

Some might overcomplicate by implementing Union-Find:

# Overly complex for this problem
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
    # ... union and find methods

While Union-Find would work, it's overkill here. The sorted nature of nums allows the simpler linear scan approach with O(n) time and space complexity.

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

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More