3323. Minimize Connected Groups by Inserting Interval 🔒
Problem Description
You have a collection of intervals represented as a 2D array intervals
, where each intervals[i] = [start_i, end_i]
defines the start and end points of interval i
. You're also given an integer k
.
Your task is to add exactly one new interval [start_new, end_new]
to the array with the following constraints:
- The length of the new interval (
end_new - start_new
) must be at mostk
- After adding this interval, you want to minimize the number of connected groups
A connected group is a maximal set of intervals that together cover a continuous range without any gaps. When intervals overlap or touch each other, they form a single connected group.
For example:
- The intervals
[[1, 2], [2, 5], [3, 3]]
form one connected group because they collectively cover the range [1, 5] without gaps - The intervals
[[1, 2], [3, 4]]
form two connected groups because there's a gap between 2 and 3
The goal is to strategically place your new interval (with length at most k
) to connect as many existing groups as possible, thereby minimizing the total number of connected groups in the final configuration.
Return the minimum possible number of connected groups after adding exactly one new interval.
Intuition
The key insight is that we want to use our new interval to bridge gaps between existing connected groups. Since intervals within a connected group are already touching or overlapping, adding a new interval inside a group won't reduce the total number of groups.
First, we need to identify the current connected groups. We can do this by sorting the intervals and merging overlapping ones. After merging, each element in our merged
array represents a distinct connected group, and the gaps between consecutive groups are where we can potentially place our new interval to connect them.
The strategic question becomes: where should we place an interval of length at most k
to connect the maximum number of groups?
Consider that if we start our new interval right after some group ends (at position e
where e
is the end of a group), the new interval can extend up to e + k
. This new interval would connect all groups that start within the range [e, e + k]
.
To maximize the impact, we enumerate each group's ending position as a potential starting point for our bridging interval. For each ending position e
of group i
, we use binary search to find the first group j
whose starting position is greater than e + k
. This means groups from i
to j-1
can all be connected by placing our new interval starting at or near e
.
The number of groups that get merged together is j - i
, so the reduction in total groups is j - i - 1
(since these j - i
groups become 1 group). Therefore, the resulting number of groups would be len(merged) - (j - i - 1)
.
By trying all possible positions and taking the minimum, we find the optimal placement for our new interval.
Learn more about Binary Search, Sorting and Sliding Window patterns.
Solution Approach
The implementation follows a three-step process: merge intervals, find optimal placement, and calculate the minimum groups.
Step 1: Sort and Merge Overlapping Intervals
First, we sort the intervals by their starting points using intervals.sort()
. Then we merge overlapping intervals to identify the current connected groups:
merged = [intervals[0]]
for s, e in intervals[1:]:
if merged[-1][1] < s:
merged.append([s, e])
else:
merged[-1][1] = max(merged[-1][1], e)
For each interval [s, e]
, we check if it overlaps with the last interval in merged
:
- If
merged[-1][1] < s
(the last interval's end is before the current interval's start), they don't overlap, so we add the current interval as a new group - Otherwise, they overlap or touch, so we extend the last interval's end to
max(merged[-1][1], e)
After this step, merged
contains all the connected groups, and len(merged)
gives us the initial number of groups.
Step 2: Find Optimal Placement Using Binary Search
We initialize ans = len(merged)
as our baseline (no reduction in groups). Then for each group i
with ending position e
:
for i, (_, e) in enumerate(merged):
j = bisect_left(merged, [e + k + 1, 0])
ans = min(ans, len(merged) - (j - i - 1))
The binary search bisect_left(merged, [e + k + 1, 0])
finds the index j
of the first group whose starting position is greater than or equal to e + k + 1
. This means:
- Groups from index
i
toj-1
can all be connected by placing an interval starting ate
with lengthk
- The new interval
[e, e + k]
would bridge thesej - i
groups into one connected group - The reduction in total groups is
j - i - 1
- The resulting number of groups is
len(merged) - (j - i - 1)
Step 3: Return the Minimum
After trying all possible positions (each group's ending point), we return the minimum number of groups achievable.
The time complexity is O(n log n)
for sorting plus O(n log n)
for the binary searches, resulting in O(n log n)
overall. The space complexity is O(n)
for storing the merged intervals.
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 a concrete example:
Input: intervals = [[1, 3], [5, 6], [8, 10], [11, 13]]
, k = 3
Step 1: Sort and Merge The intervals are already sorted. Since none of them overlap (there are gaps between each pair), the merged array remains the same:
merged = [[1, 3], [5, 6], [8, 10], [11, 13]]
- Initial number of groups = 4
Step 2: Try Each Position
Now we try placing our new interval (of length ≤ 3) starting from each group's end:
Position 1: Start after group 0 ends (at position 3)
- New interval could be
[3, 6]
(length = 3) - This connects groups at indices 0 and 1 (since group 1 starts at 5, which is ≤ 6)
- Binary search:
bisect_left(merged, [3 + 3 + 1, 0])
=bisect_left(merged, [7, 0])
= 2 - Groups 0 to 1 get connected (2 groups become 1)
- Reduction = 2 - 0 - 1 = 1
- Result = 4 - 1 = 3 groups
Position 2: Start after group 1 ends (at position 6)
- New interval could be
[6, 9]
(length = 3) - This connects groups at indices 1 and 2 (since group 2 starts at 8, which is ≤ 9)
- Binary search:
bisect_left(merged, [6 + 3 + 1, 0])
=bisect_left(merged, [10, 0])
= 2 - Groups 1 to 1 get connected (just group 1 itself)
- Reduction = 2 - 1 - 1 = 0
- Result = 4 - 0 = 4 groups
Position 3: Start after group 2 ends (at position 10)
- New interval could be
[10, 13]
(length = 3) - This connects groups at indices 2 and 3 (since group 3 starts at 11, which is ≤ 13)
- Binary search:
bisect_left(merged, [10 + 3 + 1, 0])
=bisect_left(merged, [14, 0])
= 4 - Groups 2 to 3 get connected (2 groups become 1)
- Reduction = 4 - 2 - 1 = 1
- Result = 4 - 1 = 3 groups
Position 4: Start after group 3 ends (at position 13)
- New interval could be
[13, 16]
(length = 3) - No groups start within range [13, 16]
- Binary search:
bisect_left(merged, [13 + 3 + 1, 0])
=bisect_left(merged, [17, 0])
= 4 - Groups 3 to 3 get connected (just group 3 itself)
- Reduction = 4 - 3 - 1 = 0
- Result = 4 - 0 = 4 groups
Step 3: Return Minimum The minimum across all positions is 3 groups (achieved at positions 1 and 3).
Therefore, the answer is 3.
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4
5class Solution:
6 def minConnectedGroups(self, intervals: List[List[int]], k: int) -> int:
7 # Sort intervals by start time
8 intervals.sort()
9
10 # Merge overlapping intervals
11 merged_intervals = [intervals[0]]
12 for start, end in intervals[1:]:
13 # If current interval doesn't overlap with the last merged interval
14 if merged_intervals[-1][1] < start:
15 merged_intervals.append([start, end])
16 else:
17 # Extend the last merged interval's end time
18 merged_intervals[-1][1] = max(merged_intervals[-1][1], end)
19
20 # Initialize answer with total number of merged intervals
21 min_groups = len(merged_intervals)
22
23 # Try connecting intervals within distance k
24 for i, (_, end_time) in enumerate(merged_intervals):
25 # Find the first interval that cannot be connected within distance k
26 # We look for intervals starting after end_time + k
27 j = bisect_left(merged_intervals, [end_time + k + 1, 0])
28
29 # Calculate groups after connecting intervals from i to j-1
30 # Connected intervals: (j - i - 1) intervals can be merged into one group
31 # Remaining groups: len(merged_intervals) - (j - i - 1)
32 min_groups = min(min_groups, len(merged_intervals) - (j - i - 1))
33
34 return min_groups
35
1class Solution {
2 /**
3 * Finds the minimum number of connected groups after potentially connecting intervals within distance k.
4 * @param intervals array of intervals where each interval is [start, end]
5 * @param k maximum distance allowed to connect two intervals
6 * @return minimum number of connected groups
7 */
8 public int minConnectedGroups(int[][] intervals, int k) {
9 // Sort intervals by their start points in ascending order
10 Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
11
12 // Merge overlapping intervals first
13 List<int[]> mergedIntervals = new ArrayList<>();
14 mergedIntervals.add(intervals[0]);
15
16 for (int i = 1; i < intervals.length; i++) {
17 int[] currentInterval = intervals[i];
18 int[] lastMerged = mergedIntervals.get(mergedIntervals.size() - 1);
19
20 // Check if current interval overlaps with the last merged interval
21 if (lastMerged[1] < currentInterval[0]) {
22 // No overlap, add as new interval
23 mergedIntervals.add(currentInterval);
24 } else {
25 // Overlap exists, merge by extending the end point
26 lastMerged[1] = Math.max(lastMerged[1], currentInterval[1]);
27 }
28 }
29
30 // Initialize result with total number of merged intervals (worst case)
31 int minGroups = mergedIntervals.size();
32
33 // Try connecting each interval with others within distance k
34 for (int i = 0; i < mergedIntervals.size(); i++) {
35 int[] currentInterval = mergedIntervals.get(i);
36
37 // Find the first interval that cannot be connected with current interval
38 // (i.e., first interval whose start > currentInterval.end + k)
39 int firstUnconnectable = binarySearch(mergedIntervals, currentInterval[1] + k + 1);
40
41 // Calculate groups if we connect all intervals from i to firstUnconnectable-1
42 // Connected intervals: from index i to firstUnconnectable-1
43 // Number of intervals that get connected: firstUnconnectable - i - 1
44 // Remaining groups: total - (intervals that got connected)
45 int groupsAfterConnection = mergedIntervals.size() - (firstUnconnectable - i - 1);
46 minGroups = Math.min(minGroups, groupsAfterConnection);
47 }
48
49 return minGroups;
50 }
51
52 /**
53 * Binary search to find the first interval whose start point is >= target value.
54 * @param intervals list of sorted intervals
55 * @param targetStart target start value to search for
56 * @return index of first interval with start >= targetStart, or intervals.size() if none exists
57 */
58 private int binarySearch(List<int[]> intervals, int targetStart) {
59 int left = 0;
60 int right = intervals.size();
61
62 while (left < right) {
63 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
64
65 if (intervals.get(mid)[0] >= targetStart) {
66 // Mid interval starts at or after target, search left half
67 right = mid;
68 } else {
69 // Mid interval starts before target, search right half
70 left = mid + 1;
71 }
72 }
73
74 return left;
75 }
76}
77
1class Solution {
2public:
3 int minConnectedGroups(vector<vector<int>>& intervals, int k) {
4 // Sort intervals by start time
5 sort(intervals.begin(), intervals.end());
6
7 // Merge overlapping intervals
8 vector<vector<int>> mergedIntervals;
9 for (const auto& currentInterval : intervals) {
10 int start = currentInterval[0];
11 int end = currentInterval[1];
12
13 // If no merged intervals yet or current doesn't overlap with last merged
14 if (mergedIntervals.empty() || mergedIntervals.back()[1] < start) {
15 mergedIntervals.emplace_back(currentInterval);
16 } else {
17 // Extend the last merged interval's end time
18 mergedIntervals.back()[1] = max(mergedIntervals.back()[1], end);
19 }
20 }
21
22 // Initialize result with total number of merged intervals
23 int minGroups = mergedIntervals.size();
24
25 // Try connecting each interval with others within distance k
26 for (int i = 0; i < mergedIntervals.size(); ++i) {
27 const auto& currentInterval = mergedIntervals[i];
28
29 // Find the first interval that starts after current interval's end + k
30 // This gives us the range of intervals that can be connected
31 int nextUnreachableIndex = lower_bound(
32 mergedIntervals.begin(),
33 mergedIntervals.end(),
34 vector<int>{currentInterval[1] + k + 1, 0}
35 ) - mergedIntervals.begin();
36
37 // Calculate groups if we connect intervals from i to nextUnreachableIndex-1
38 // Connected intervals: (nextUnreachableIndex - i - 1)
39 // Remaining groups: total - connected + 1 (the connected group)
40 int groupsAfterConnection = mergedIntervals.size() - (nextUnreachableIndex - i - 1);
41 minGroups = min(minGroups, groupsAfterConnection);
42 }
43
44 return minGroups;
45 }
46};
47
1/**
2 * Finds the minimum number of connected groups after merging intervals within distance k
3 * @param intervals - Array of intervals represented as [start, end] pairs
4 * @param k - Maximum distance to connect intervals
5 * @returns Minimum number of connected groups
6 */
7function minConnectedGroups(intervals: number[][], k: number): number {
8 // Sort intervals by start position
9 intervals.sort((a, b) => a[0] - b[0]);
10
11 // Merge overlapping intervals
12 const mergedIntervals: number[][] = [];
13 for (const interval of intervals) {
14 const [start, end] = interval;
15
16 // If no merged intervals yet or current interval doesn't overlap with last merged
17 if (mergedIntervals.length === 0 || mergedIntervals[mergedIntervals.length - 1][1] < start) {
18 mergedIntervals.push(interval);
19 } else {
20 // Extend the last merged interval's end point if current interval overlaps
21 mergedIntervals[mergedIntervals.length - 1][1] = Math.max(
22 mergedIntervals[mergedIntervals.length - 1][1],
23 end
24 );
25 }
26 }
27
28 /**
29 * Binary search to find the first interval with start >= target value
30 * @param target - Target value to search for
31 * @returns Index of first interval with start >= target
32 */
33 const binarySearch = (target: number): number => {
34 let left = 0;
35 let right = mergedIntervals.length;
36
37 while (left < right) {
38 const mid = Math.floor((left + right) / 2);
39
40 if (mergedIntervals[mid][0] >= target) {
41 right = mid;
42 } else {
43 left = mid + 1;
44 }
45 }
46
47 return left;
48 };
49
50 // Initialize answer with total number of merged intervals (worst case: no connections)
51 let minGroups = mergedIntervals.length;
52
53 // Try connecting each interval with intervals within distance k
54 for (let i = 0; i < mergedIntervals.length; i++) {
55 // Find first interval that cannot be connected (start > current end + k)
56 const nextUnconnectableIndex = binarySearch(mergedIntervals[i][1] + k + 1);
57
58 // Calculate number of intervals that can be connected (from i to nextUnconnectableIndex - 1)
59 const connectedCount = nextUnconnectableIndex - i - 1;
60
61 // Calculate total groups if we connect intervals from i to nextUnconnectableIndex - 1
62 const groupsAfterConnection = mergedIntervals.length - connectedCount;
63
64 minGroups = Math.min(minGroups, groupsAfterConnection);
65 }
66
67 return minGroups;
68}
69
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is dominated by the following operations:
intervals.sort()
: This sorting operation takesO(n × log n)
time wheren
is the number of intervals- First loop to merge overlapping intervals:
O(n)
time as it iterates through all intervals once - Second loop with binary search: The outer loop runs at most
O(n)
times (once for each merged interval), and for each iteration,bisect_left
performs a binary search which takesO(log n)
time. This gives usO(n × log n)
in the worst case
Since O(n × log n) + O(n) + O(n × log n) = O(n × log n)
, the overall time complexity is O(n × log n)
.
Space Complexity: O(n)
The space complexity comes from:
merged
list: In the worst case where no intervals overlap, this list stores alln
intervals, requiringO(n)
space- The sorting operation may use
O(log n)
space for the recursion stack (depending on the implementation), but this is dominated byO(n)
Therefore, the overall space complexity is O(n)
where n
is the number of intervals.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrectly Calculating the Reduction in Groups
The Problem:
A common mistake is misunderstanding how many groups are actually being connected. When placing an interval starting at position e
with length k
, developers often incorrectly calculate the reduction as j - i
instead of j - i - 1
.
Why It Happens:
The confusion arises from counting the groups being connected. If we connect groups from index i
to j-1
, we have j - i
groups total. However, these j - i
groups merge into 1 group, so the reduction is (j - i) - 1 = j - i - 1
.
Example:
If we have 3 groups [[1,2], [5,6], [10,11]]
and our new interval connects all three, we go from 3 groups to 1 group - a reduction of 2, not 3.
Solution:
Always remember that connecting n
groups results in a reduction of n - 1
groups:
# Correct calculation
reduction = j - i - 1
min_groups = min(min_groups, len(merged_intervals) - reduction)
Pitfall 2: Off-by-One Error in Binary Search Target
The Problem:
Using bisect_left(merged_intervals, [e + k, 0])
instead of bisect_left(merged_intervals, [e + k + 1, 0])
when finding which intervals can be connected.
Why It Happens:
The new interval [e, e + k]
has an endpoint at e + k
. An existing interval [s, end]
can be connected if s <= e + k
(they touch or overlap). We want to find the first interval that CANNOT be connected, which requires s > e + k
, or equivalently s >= e + k + 1
.
Example:
If e = 5
and k = 3
, the new interval is [5, 8]
. An interval starting at 8 CAN be connected (they touch), but an interval starting at 9 cannot.
Solution: Use the correct search target:
# Find first interval that CANNOT be connected j = bisect_left(merged_intervals, [e + k + 1, 0])
Pitfall 3: Not Considering Edge Cases for Interval Placement
The Problem: Only considering placing the new interval at the end of each merged group, missing opportunities to place it at the beginning or in gaps between groups.
Why It Happens: The code only iterates through ending positions of merged intervals, which might not cover all optimal placements, especially when placing an interval at the very beginning could connect multiple groups.
Solution: Consider additional placement strategies:
# Also try placing interval before the first group
if len(merged_intervals) > 0:
first_start = merged_intervals[0][0]
j = bisect_left(merged_intervals, [first_start - k, 0])
# Find how many groups can be connected from the beginning
connected = 0
for idx in range(len(merged_intervals)):
if merged_intervals[idx][0] <= first_start:
connected = idx + 1
else:
break
min_groups = min(min_groups, len(merged_intervals) - connected + 1)
Which data structure is used to implement priority queue?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Want a Structured Path to Master System Design Too? Don’t Miss This!