Facebook Pixel

2580. Count Ways to Group Overlapping Ranges

Problem Description

You have a collection of integer ranges, where each range is represented as [start, end] containing all integers from start to end (inclusive).

Your task is to split these ranges into exactly two groups (either group can be empty) with one important constraint: if two ranges overlap (share at least one common integer), they must be placed in the same group.

For example:

  • Ranges [1, 3] and [2, 5] overlap because they both contain the integers 2 and 3
  • Ranges [1, 2] and [3, 4] do not overlap

The problem asks you to count the total number of different ways to split all the ranges into two groups while respecting the overlap constraint. Since this number can be very large, return the result modulo 10^9 + 7.

The key insight is that overlapping ranges form connected components - if range A overlaps with range B, and range B overlaps with range C, then all three must be in the same group. Each connected component of overlapping ranges can independently be assigned to either group 1 or group 2, giving us 2^n possible arrangements where n is the number of connected components.

The solution sorts the ranges and merges overlapping ones to identify these connected components. Each time a new non-overlapping component is found (when the start of a range is greater than the maximum end seen so far), it increments the component count. The final answer is 2^cnt where cnt is the number of connected components.

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

Intuition

The first observation is that if two ranges overlap, they must go into the same group - they're "stuck together". This creates a chain reaction: if range A overlaps with B, and B overlaps with C, then A, B, and C must all be in the same group, even if A and C don't directly overlap.

This naturally leads us to think about connected components. We can visualize the ranges as nodes in a graph where edges connect overlapping ranges. Each connected component in this graph must be placed entirely in one group or the other - we can't split a component.

To find these components efficiently, we can sort the ranges by their starting points. As we scan through the sorted ranges, we maintain the maximum ending point seen so far. When we encounter a range whose start is greater than this maximum end, we know we've found a new independent component that doesn't overlap with any previous ranges.

For example, with ranges [[1,3], [2,5], [7,9]]:

  • After sorting: [[1,3], [2,5], [7,9]]
  • Process [1,3]: max end = 3
  • Process [2,5]: start (2) ≀ max end (3), so it overlaps. Update max end = 5
  • Process [7,9]: start (7) > max end (5), so this is a new component. Count increases.

Once we know we have cnt independent components, each component can independently choose to go into group 1 or group 2. This gives us 2 choices for the first component, 2 choices for the second, and so on, resulting in 2^cnt total ways to split the ranges.

The sorting step ensures we can identify all overlapping ranges in a single pass, making the solution efficient.

Learn more about Sorting patterns.

Solution Approach

The implementation follows these key steps:

1. Sort the ranges:

ranges.sort()

By sorting the ranges based on their starting points (default behavior for lists), we ensure that we can process them in order and identify overlapping groups in a single pass.

2. Count connected components:

cnt, mx = 0, -1
for start, end in ranges:
    if start > mx:
        cnt += 1
    mx = max(mx, end)

We initialize:

  • cnt = 0: Counter for the number of connected components
  • mx = -1: Tracks the maximum ending point seen so far (initialized to -1 to handle the first range)

For each range [start, end]:

  • If start > mx, this range doesn't overlap with any previous ranges, so it starts a new component. We increment cnt.
  • Update mx to be the maximum of the current mx and end to extend our "coverage" for detecting future overlaps.

3. Calculate the result using fast power:

mod = 10**9 + 7
return pow(2, cnt, mod)

Since each of the cnt components can independently be placed in either of the 2 groups, the total number of ways is 2^cnt. Python's built-in pow(base, exp, mod) efficiently computes (base^exp) % mod using fast exponentiation.

Time Complexity: O(n log n) where n is the number of ranges

  • Sorting takes O(n log n)
  • The single pass through ranges takes O(n)
  • Fast power takes O(log cnt) which is at most O(log n)

Space Complexity: O(log n) for the sorting algorithm's recursion stack

Alternative without fast power: As mentioned in the reference, we could also multiply by 2 each time we find a new component:

result = 1
for start, end in ranges:
    if start > mx:
        result = (result * 2) % mod
    mx = max(mx, end)
return result

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 the solution with ranges: [[1,3], [5,7], [2,4], [8,10], [6,9]]

Step 1: Sort the ranges by start point After sorting: [[1,3], [2,4], [5,7], [6,9], [8,10]]

Step 2: Identify connected components

Initialize cnt = 0 (component counter) and mx = -1 (max end seen)

  • Process [1,3]:

    • start=1 > mx=-1? Yes β†’ New component! cnt = 1
    • Update mx = max(-1, 3) = 3
  • Process [2,4]:

    • start=2 > mx=3? No β†’ Same component (overlaps with previous)
    • Update mx = max(3, 4) = 4
  • Process [5,7]:

    • start=5 > mx=4? Yes β†’ New component! cnt = 2
    • Update mx = max(4, 7) = 7
  • Process [6,9]:

    • start=6 > mx=7? No β†’ Same component (overlaps with [5,7])
    • Update mx = max(7, 9) = 9
  • Process [8,10]:

    • start=8 > mx=9? No β†’ Same component (overlaps with [6,9])
    • Update mx = max(9, 10) = 10

Step 3: Calculate result

We found cnt = 2 connected components:

  • Component 1: {[1,3], [2,4]} (these overlap and must stay together)
  • Component 2: {[5,7], [6,9], [8,10]} (these form an overlapping chain)

Each component can independently go to either Group A or Group B:

  • Component 1 β†’ Group A, Component 2 β†’ Group A
  • Component 1 β†’ Group A, Component 2 β†’ Group B
  • Component 1 β†’ Group B, Component 2 β†’ Group A
  • Component 1 β†’ Group B, Component 2 β†’ Group B

Total ways = 2^2 = 4

The algorithm returns pow(2, 2, 10^9+7) = 4

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def countWays(self, ranges: List[List[int]]) -> int:
6        """
7        Count the number of ways to partition ranges into groups.
8        Each group contains overlapping or adjacent ranges.
9      
10        Args:
11            ranges: List of ranges where each range is [start, end]
12      
13        Returns:
14            Number of ways to select groups (2^number_of_groups) modulo 10^9 + 7
15        """
16        # Sort ranges by their start position for sequential processing
17        ranges.sort()
18      
19        # Initialize counter for number of disjoint groups and maximum end point seen
20        group_count = 0
21        max_end = -1
22      
23        # Iterate through sorted ranges to identify disjoint groups
24        for start, end in ranges:
25            # If current range starts after the maximum end seen so far,
26            # it begins a new disjoint group
27            if start > max_end:
28                group_count += 1
29          
30            # Update the maximum end point seen
31            max_end = max(max_end, end)
32      
33        # Define modulo constant for large number handling
34        MOD = 10**9 + 7
35      
36        # Return 2^group_count mod MOD (number of ways to select from groups)
37        return pow(2, group_count, MOD)
38
1class Solution {
2    /**
3     * Counts the number of ways to select non-overlapping groups from ranges.
4     * Two ranges can be in the same group if they overlap or connect.
5     * 
6     * @param ranges Array of integer ranges where each range is [start, end]
7     * @return Number of ways to select groups modulo 10^9 + 7
8     */
9    public int countWays(int[][] ranges) {
10        // Sort ranges by their starting points in ascending order
11        Arrays.sort(ranges, (a, b) -> a[0] - b[0]);
12      
13        // Count number of disjoint groups of overlapping ranges
14        int groupCount = 0;
15        int maxEndPoint = -1;
16      
17        // Iterate through sorted ranges to identify disjoint groups
18        for (int[] range : ranges) {
19            // If current range starts after the maximum end point seen so far,
20            // it belongs to a new group (no overlap with previous ranges)
21            if (range[0] > maxEndPoint) {
22                groupCount++;
23            }
24            // Update the maximum end point encountered
25            maxEndPoint = Math.max(maxEndPoint, range[1]);
26        }
27      
28        // Each group can either be selected or not selected (2 choices per group)
29        // Total ways = 2^groupCount
30        int modulo = (int) 1e9 + 7;
31        return quickPower(2, groupCount, modulo);
32    }
33  
34    /**
35     * Calculates (base^exponent) % modulo using binary exponentiation.
36     * 
37     * @param base The base number
38     * @param exponent The power to raise the base to
39     * @param modulo The modulo value
40     * @return (base^exponent) % modulo
41     */
42    private int quickPower(long base, int exponent, int modulo) {
43        long result = 1;
44      
45        // Binary exponentiation algorithm
46        while (exponent > 0) {
47            // If current bit of exponent is 1, multiply result by current base
48            if ((exponent & 1) == 1) {
49                result = (result * base) % modulo;
50            }
51            // Square the base for the next bit position
52            base = (base * base) % modulo;
53            // Shift exponent right by 1 bit (divide by 2)
54            exponent >>= 1;
55        }
56      
57        return (int) result;
58    }
59}
60
1class Solution {
2public:
3    int countWays(vector<vector<int>>& ranges) {
4        // Sort ranges by start position
5        sort(ranges.begin(), ranges.end());
6      
7        // Count number of disjoint groups
8        int groupCount = 0;
9        int maxEndPoint = -1;
10      
11        // Iterate through sorted ranges to find disjoint groups
12        for (auto& range : ranges) {
13            // If current range starts after the maximum end point seen so far,
14            // it forms a new disjoint group
15            if (range[0] > maxEndPoint) {
16                groupCount++;
17            }
18            // Update the maximum end point
19            maxEndPoint = max(maxEndPoint, range[1]);
20        }
21      
22        // Define long long type alias for modular arithmetic
23        using ll = long long;
24      
25        // Fast exponentiation function to calculate (base^exponent) % mod
26        auto fastPower = [&](ll base, int exponent, int mod) {
27            ll result = 1;
28          
29            // Binary exponentiation algorithm
30            while (exponent > 0) {
31                // If current bit is 1, multiply result by base
32                if (exponent & 1) {
33                    result = result * base % mod;
34                }
35                // Square the base for next bit
36                base = base * base % mod;
37                // Right shift to process next bit
38                exponent >>= 1;
39            }
40          
41            return result;
42        };
43      
44        // Each disjoint group can be selected in 2 ways (include or exclude)
45        // Total ways = 2^groupCount
46        const int MOD = 1e9 + 7;
47        return fastPower(2, groupCount, MOD);
48    }
49};
50
1/**
2 * Counts the number of ways to select ranges such that no two selected ranges overlap.
3 * @param ranges - Array of ranges where each range is [start, end]
4 * @returns The number of ways to select non-overlapping ranges modulo 10^9 + 7
5 */
6function countWays(ranges: number[][]): number {
7    // Sort ranges by their start position in ascending order
8    ranges.sort((a, b) => a[0] - b[0]);
9  
10    // Track the maximum end position seen so far
11    let maxEndPosition: number = -1;
12  
13    // Initialize answer to 1 (representing the empty selection)
14    let answer: number = 1;
15  
16    // Define modulo constant for preventing integer overflow
17    const MODULO: number = 10 ** 9 + 7;
18  
19    // Iterate through each sorted range
20    for (const [startPosition, endPosition] of ranges) {
21        // If current range starts after the maximum end position seen so far,
22        // it means we have a new group of overlapping ranges
23        if (startPosition > maxEndPosition) {
24            // Each group can either be included or excluded, so multiply by 2
25            answer = (answer * 2) % MODULO;
26        }
27      
28        // Update the maximum end position
29        maxEndPosition = Math.max(maxEndPosition, endPosition);
30    }
31  
32    return answer;
33}
34

Time and Space Complexity

Time Complexity: O(n log n) where n is the number of ranges in the input list.

  • The ranges.sort() operation takes O(n log n) time
  • The for loop iterates through all ranges once, which takes O(n) time
  • Inside the loop, all operations (comparison, max, increment) are O(1)
  • The pow(2, cnt, mod) operation takes O(log cnt) time, and since cnt ≀ n, this is O(log n)
  • Overall: O(n log n) + O(n) + O(log n) = O(n log n)

Space Complexity: O(1) or O(n) depending on the sorting algorithm implementation.

  • If we consider the space used by the sorting algorithm:
    • Python's sort() uses Timsort, which has O(n) worst-case space complexity
    • This gives us O(n) space complexity overall
  • If we only consider auxiliary space excluding the sorting:
    • Only a constant number of variables are used: cnt, mx, mod, start, end
    • This gives us O(1) auxiliary space complexity
  • The most accurate answer is O(n) due to the sorting algorithm's space requirements

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

Common Pitfalls

1. Incorrect Overlap Detection Logic

Pitfall: A common mistake is using start >= mx instead of start > mx when checking for a new component. This would incorrectly treat ranges that share exactly one endpoint as non-overlapping.

Example:

  • Ranges: [[1, 3], [3, 5]]
  • These ranges overlap at point 3 and should be in the same component
  • Using start >= mx would incorrectly count them as separate components

Solution: Always use strict inequality start > mx to ensure ranges that touch at endpoints are correctly identified as overlapping.

2. Integer Overflow in Power Calculation

Pitfall: Computing 2^cnt without modular arithmetic can cause integer overflow, especially when the number of components is large.

Incorrect approach:

result = 2 ** cnt  # Can overflow for large cnt
return result % MOD

Solution: Use Python's built-in pow(2, cnt, MOD) which performs modular exponentiation efficiently, or apply modulo at each multiplication step:

result = 1
for _ in range(cnt):
    result = (result * 2) % MOD

3. Not Handling Edge Cases

Pitfall: Failing to handle empty input or single range cases properly.

Example issues:

  • Empty list ranges = [] should return 1 (one way to split zero ranges)
  • Initial mx = 0 instead of mx = -1 could cause issues with ranges starting at 0

Solution: Initialize mx = -1 to handle all valid range starts correctly, and the algorithm naturally handles empty input (returns 2^0 = 1).

4. Forgetting to Sort Ranges

Pitfall: Processing ranges in their original order without sorting can miss overlaps between non-adjacent ranges in the input.

Example:

  • Input: [[3, 5], [1, 2], [2, 4]]
  • Without sorting, you might miss that [1, 2] and [2, 4] overlap, and both overlap with [3, 5]

Solution: Always sort ranges by their start position first: ranges.sort()

5. Using Wrong Modulo Value

Pitfall: Using incorrect modulo value (e.g., 10^9 + 9 or 1e9 + 7 which gives a float).

Solution: Use exactly MOD = 10**9 + 7 (or 1000000007) as specified in the problem.

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

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More