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.
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 componentsmx = -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 incrementcnt
. - Update
mx
to be the maximum of the currentmx
andend
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 mostO(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 EvaluatorExample 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 takesO(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 takesO(log cnt)
time, and sincecnt β€ n
, this isO(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 hasO(n)
worst-case space complexity - This gives us
O(n)
space complexity overall
- Python's
- 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
- Only a constant number of variables are used:
- 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 ofmx = -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.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!