LeetCode 3440 - Reschedule Meetings for Maximum Free Time II
We are given an event that lasts from time 0 to eventTime. Inside this event there are n non-overlapping meetings, represented by the arrays startTime and endTime.
Difficulty: 🟡 Medium
Topics: Array, Greedy, Enumeration
Solution
LeetCode 3440 - Reschedule Meetings for Maximum Free Time II
Problem Understanding
We are given an event that lasts from time 0 to eventTime. Inside this event there are n non-overlapping meetings, represented by the arrays startTime and endTime.
The meeting at index i occupies the interval:
[startTime[i], endTime[i]]
The meetings are already sorted and non-overlapping, meaning:
endTime[i] <= startTime[i + 1]
The goal is to maximize the length of the longest continuous free interval during the event.
Unlike the first version of this problem, we are allowed to move one meeting anywhere within the event as long as:
- Its duration remains unchanged.
- It stays completely inside
[0, eventTime]. - It does not overlap any other meeting.
- The relative order of meetings is allowed to change.
We may also choose not to move any meeting.
The output is the maximum possible length of a continuous free time interval after performing at most one rescheduling operation.
The constraints are important:
ncan be as large as100000.eventTimecan be as large as10^9.
Because of the large value of n, any algorithm worse than roughly O(n log n) is likely too slow. An O(n²) solution is completely infeasible.
Several edge cases are worth noting:
- A meeting may already touch the beginning of the event.
- A meeting may already touch the end of the event.
- There may be no free time at all.
- Moving a meeting might completely eliminate it from a region and merge two neighboring gaps.
- A meeting can potentially be relocated into some other free gap elsewhere in the schedule.
- Since ordering may change, we only care whether a meeting can fit into some free gap, not whether it remains between the same neighbors.
Approaches
Brute Force
A straightforward approach is to try every meeting as the one to reschedule.
For a chosen meeting:
- Remove it from the schedule.
- Compute the free interval created by its removal.
- Enumerate every possible gap where that meeting might be reinserted.
- Simulate the insertion.
- Compute the longest free interval after insertion.
Since there are n meetings and potentially O(n) candidate gaps for each meeting, this already leads to at least O(n²) work.
With n = 100000, such a solution is far too slow.
Key Observation
Suppose we remove meeting i.
Let:
duration = endTime[i] - startTime[i]
Its neighboring boundaries are:
leftBoundary = endTime[i-1] (or 0)
rightBoundary = startTime[i+1] (or eventTime)
Removing the meeting merges:
- the gap before it,
- the meeting itself,
- the gap after it.
The resulting free interval becomes:
mergedGap = rightBoundary - leftBoundary
Now we have two possibilities.
Case 1: Move the meeting somewhere else
If there exists another free gap, outside this merged region, whose length is at least duration, then the meeting can be completely relocated there.
In that situation, the entire merged interval remains free:
free = mergedGap
Case 2: The meeting cannot fit elsewhere
Then the meeting must remain inside the merged interval.
After placing a meeting of length duration somewhere inside an interval of size mergedGap, the largest remaining free segment is:
mergedGap - duration
Therefore for each meeting we only need to know:
- the merged interval length,
- whether some other gap has length at least the meeting duration.
This transforms the problem into a linear scan problem.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Simulate removing and reinserting every meeting |
| Optimal | O(n) | O(n) | Prefix/suffix maximum gap queries |
Algorithm Walkthrough
Step 1: Compute all free gaps
There are n + 1 free gaps.
The first gap is:
startTime[0] - 0
The last gap is:
eventTime - endTime[n-1]
The internal gaps are:
startTime[i] - endTime[i-1]
Store them in an array gaps.
Step 2: Build prefix maximums
Create:
prefixMax[i]
which stores the maximum gap among:
gaps[0...i]
This allows us to query the largest gap to the left of any position in O(1) time.
Step 3: Build suffix maximums
Create:
suffixMax[i]
which stores the maximum gap among:
gaps[i...n]
This allows us to query the largest gap to the right of any position in O(1) time.
Step 4: Evaluate each meeting
For meeting i:
duration = endTime[i] - startTime[i]
Its neighboring boundaries are:
leftBoundary =
0 if i == 0
endTime[i-1] otherwise
rightBoundary =
eventTime if i == n-1
startTime[i+1] otherwise
The merged interval after removing the meeting is:
mergedGap = rightBoundary - leftBoundary
Step 5: Find the largest gap outside the merged region
Meeting i is surrounded by:
gaps[i]
gaps[i+1]
Those two gaps participate in the merged interval and therefore cannot be used as an external relocation target.
We need the largest gap among all other gaps.
Using prefix and suffix maxima:
bestOutside =
max(gaps before i,
gaps after i+1)
Specifically:
max(prefixMax[i-1], suffixMax[i+2])
when those indices exist.
Step 6: Decide whether relocation is possible
If:
bestOutside >= duration
then the meeting can be moved completely outside the merged interval.
The resulting free interval is:
mergedGap
Otherwise the meeting must stay inside that interval.
The largest free segment becomes:
mergedGap - duration
Update the global answer.
Step 7: Return the maximum answer
The maximum value found across all meetings is the result.
Why it works
Removing a meeting merges its adjacent gaps into one interval. Any optimal rescheduling must either place the meeting somewhere outside this merged interval or somewhere inside it. If another gap can accommodate the meeting, the merged interval remains entirely free and contributes mergedGap free time. Otherwise the meeting must consume duration units inside the merged interval, leaving at most mergedGap - duration continuous free time. Since prefix and suffix maximum arrays let us determine whether such an external gap exists, every meeting can be evaluated independently and optimally.
Python Solution
from typing import List
class Solution:
def maxFreeTime(self, eventTime: int, startTime: List[int], endTime: List[int]) -> int:
n = len(startTime)
gaps = [0] * (n + 1)
gaps[0] = startTime[0]
for i in range(1, n):
gaps[i] = startTime[i] - endTime[i - 1]
gaps[n] = eventTime - endTime[-1]
prefix_max = [0] * (n + 1)
prefix_max[0] = gaps[0]
for i in range(1, n + 1):
prefix_max[i] = max(prefix_max[i - 1], gaps[i])
suffix_max = [0] * (n + 1)
suffix_max[n] = gaps[n]
for i in range(n - 1, -1, -1):
suffix_max[i] = max(suffix_max[i + 1], gaps[i])
answer = 0
for i in range(n):
duration = endTime[i] - startTime[i]
left_boundary = 0 if i == 0 else endTime[i - 1]
right_boundary = eventTime if i == n - 1 else startTime[i + 1]
merged_gap = right_boundary - left_boundary
best_outside = 0
if i - 1 >= 0:
best_outside = max(best_outside, prefix_max[i - 1])
if i + 2 <= n:
best_outside = max(best_outside, suffix_max[i + 2])
if best_outside >= duration:
answer = max(answer, merged_gap)
else:
answer = max(answer, merged_gap - duration)
return answer
The implementation begins by constructing the gaps array, which represents every free interval in the schedule. Since there are n meetings, there are exactly n + 1 gaps.
Next, prefix and suffix maximum arrays are built. These arrays allow constant-time queries for the largest gap on either side of any meeting.
For each meeting, we compute the interval that would become free if the meeting were removed. Then we determine whether some other gap can hold the meeting. If such a gap exists, the merged interval remains entirely free. Otherwise the meeting must occupy part of that interval, reducing the achievable free segment by its duration.
The maximum value across all meetings is returned.
Go Solution
func maxFreeTime(eventTime int, startTime []int, endTime []int) int {
n := len(startTime)
gaps := make([]int, n+1)
gaps[0] = startTime[0]
for i := 1; i < n; i++ {
gaps[i] = startTime[i] - endTime[i-1]
}
gaps[n] = eventTime - endTime[n-1]
prefixMax := make([]int, n+1)
prefixMax[0] = gaps[0]
for i := 1; i <= n; i++ {
if prefixMax[i-1] > gaps[i] {
prefixMax[i] = prefixMax[i-1]
} else {
prefixMax[i] = gaps[i]
}
}
suffixMax := make([]int, n+1)
suffixMax[n] = gaps[n]
for i := n - 1; i >= 0; i-- {
if suffixMax[i+1] > gaps[i] {
suffixMax[i] = suffixMax[i+1]
} else {
suffixMax[i] = gaps[i]
}
}
answer := 0
for i := 0; i < n; i++ {
duration := endTime[i] - startTime[i]
leftBoundary := 0
if i > 0 {
leftBoundary = endTime[i-1]
}
rightBoundary := eventTime
if i < n-1 {
rightBoundary = startTime[i+1]
}
mergedGap := rightBoundary - leftBoundary
bestOutside := 0
if i-1 >= 0 && prefixMax[i-1] > bestOutside {
bestOutside = prefixMax[i-1]
}
if i+2 <= n && suffixMax[i+2] > bestOutside {
bestOutside = suffixMax[i+2]
}
candidate := mergedGap - duration
if bestOutside >= duration {
candidate = mergedGap
}
if candidate > answer {
answer = candidate
}
}
return answer
}
The Go implementation follows the exact same logic as the Python version. Since all values are bounded by 10^9, standard Go int arithmetic is sufficient on LeetCode platforms. Arrays are implemented using slices, and prefix/suffix maximum computations are performed iteratively.
Worked Examples
Example 1
eventTime = 5
startTime = [1,3]
endTime = [2,5]
Gaps:
| Index | Gap |
|---|---|
| 0 | 1 |
| 1 | 1 |
| 2 | 0 |
Meeting 0
| Variable | Value |
|---|---|
| duration | 1 |
| leftBoundary | 0 |
| rightBoundary | 3 |
| mergedGap | 3 |
| bestOutside | 0 |
Since 0 < 1:
candidate = 3 - 1 = 2
Meeting 1
| Variable | Value |
|---|---|
| duration | 2 |
| leftBoundary | 2 |
| rightBoundary | 5 |
| mergedGap | 3 |
| bestOutside | 1 |
Since 1 < 2:
candidate = 3 - 2 = 1
Answer:
max(2, 1) = 2
Example 2
eventTime = 10
startTime = [0,7,9]
endTime = [1,8,10]
Gaps:
[0, 6, 1, 0]
For meeting 0:
| Variable | Value |
|---|---|
| duration | 1 |
| mergedGap | 7 |
| bestOutside | 1 |
Since:
bestOutside >= duration
the meeting can move elsewhere.
candidate = 7
Final answer:
7
Example 3
eventTime = 10
startTime = [0,3,7,9]
endTime = [1,4,8,10]
Gaps:
[0, 2, 3, 1, 0]
For meeting 1:
| Variable | Value |
|---|---|
| duration | 1 |
| mergedGap | 6 |
| bestOutside | 1 |
Relocation is possible.
candidate = 6
This becomes the maximum answer.
Result:
6
Example 4
eventTime = 5
startTime = [0,1,2,3,4]
endTime = [1,2,3,4,5]
Gaps:
[0,0,0,0,0,0]
Every meeting has:
mergedGap = duration
and no external gap can hold it.
Therefore:
mergedGap - duration = 0
Answer:
0
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | One pass for gaps, one pass for prefix max, one pass for suffix max, one pass to evaluate meetings |
| Space | O(n) | Gaps, prefix maximums, and suffix maximums each require linear storage |
The algorithm performs only a constant amount of work per meeting. Every array is scanned a fixed number of times, producing an overall linear runtime. This is efficient enough for n = 100000.
Test Cases
sol = Solution()
assert sol.maxFreeTime(5, [1, 3], [2, 5]) == 2 # example 1
assert sol.maxFreeTime(10, [0, 7, 9], [1, 8, 10]) == 7 # example 2
assert sol.maxFreeTime(10, [0, 3, 7, 9], [1, 4, 8, 10]) == 6 # example 3
assert sol.maxFreeTime(5, [0, 1, 2, 3, 4], [1, 2, 3, 4, 5]) == 0 # example 4
assert sol.maxFreeTime(10, [2, 5], [3, 6]) == 6 # move one meeting into another gap
assert sol.maxFreeTime(20, [5, 10], [6, 11]) == 14 # large edge gaps
assert sol.maxFreeTime(10, [1, 2], [2, 9]) == 1 # one long meeting dominates
assert sol.maxFreeTime(100, [10, 20], [11, 21]) == 89 # huge free space
assert sol.maxFreeTime(6, [0, 4], [2, 6]) == 2 # boundary-touching meetings
assert sol.maxFreeTime(8, [1, 4], [2, 5]) == 5 # relocation into another gap
assert sol.maxFreeTime(12, [2, 5, 9], [3, 6, 10]) == 6 # multiple candidate gaps
Test Summary
| Test | Why |
|---|---|
| Example 1 | Basic relocation scenario |
| Example 2 | Move first meeting to a distant gap |
| Example 3 | Middle meeting relocation |
| Example 4 | No free time exists |
[2,5] [3,6] |
Large merged interval |
| Large edge gaps | Verifies boundary handling |
| Long dominant meeting | Relocation impossible |
| Huge free space | Large gap values |
| Boundary-touching meetings | Tests start/end edge behavior |
| Relocation into another gap | Verifies external-fit logic |
| Multiple candidate gaps | Ensures maximum gap selection works |
Edge Cases
No Free Time Anywhere
When meetings completely fill the event, every gap is zero. A buggy implementation might assume there is always somewhere to move a meeting. In reality, no meeting can be relocated because no free gap exists. The algorithm correctly detects that bestOutside < duration for every meeting and returns 0.
Meeting at the Beginning or End
The first meeting has no previous meeting, and the last meeting has no next meeting. These cases require special handling when computing neighboring boundaries. The implementation uses 0 as the left boundary of the first meeting and eventTime as the right boundary of the last meeting, ensuring correct merged-gap calculations.
Relocation Changes Ordering
This version allows meeting order to change. A common mistake is to only consider moving a meeting between its original neighbors. The optimal solution instead checks whether any other gap can accommodate the meeting. Because it only cares about the existence of a sufficiently large external gap, it naturally handles order-changing relocations.
Only One External Gap Exists
Sometimes exactly one gap outside the merged interval can fit the meeting. Missing that gap would produce an incorrect answer. The prefix and suffix maximum arrays guarantee that every external gap is considered through a constant-time maximum query, so no valid relocation opportunity is overlooked.
Very Large Time Values
eventTime can be as large as 10^9. Any solution that attempts to model the timeline directly would be impossible. This implementation works only with interval endpoints and gap lengths, so its complexity depends on n, not on the magnitude of the timestamps.