7. Maximum Number of Non-Overlapping Intervals
Given an array of intervals where each interval has a start and end time, return the maximum number of non-overlapping intervals.
Examples
Example 1
Input:
meetings = [[1, 2], [2, 3], [3, 4], [1, 3]]Output:
3Explanation:
Step 1: Sort meetings by end time → [[1,2],[2,3],[1,3],[3,4]].
Step 2: Select [1,2] (count=1, last_end=2).
Step 3: [2,3] has start 2 ≥ 2, select (count=2, last_end=3).
Step 4: [1,3] start 1 < 3, skip.
Step 5: [3,4] start 3 ≥ 3, select (count=3). Result = 3.Example 2
Input:
meetings = [[0, 5], [0, 1], [1, 2], [2, 3], [3, 5], [4, 6]]Output:
4Explanation:
Step 1: Sort by end time → [[0,1],[1,2],[2,3],[3,5],[0,5],[4,6]].
Step 2: Select [0,1] (count=1, end=1).
Step 3: [1,2] (count=2, end=2).
Step 4: [2,3] (count=3, end=3).
Step 5: [3,5] (count=4, end=5). Skip [0,5] and [4,6] because their start times are before last_end=5. Result = 4.Input Format
The first line contains a single integer, meetings_rows, representing the total number of meetings.
The second line contains a single integer, meetings_columns, representing the number of properties for each meeting (typically 2 for a start and end time).
The next meetings_rows lines describe the meetings. Each line contains meetings_columns space-separated integers, representing the properties of a single meeting.
6
2
1 2
3 4
0 6
5 7
8 9
5 9Explanation:
The first line, 6, is the value for meetings_rows.
The second line, 2, is the value for meetings_columns.
The following 6 lines represent the individual meetings, which are parsed into a 2D list: [[1, 2], [3, 4], [0, 6], [5, 7], [8, 9], [5, 9]].
Constraints
0 <= meetings.length <= 1000
meetings[i].length == 2 for all 0 <= i < meetings.length
0 <= meetings[i][0] < meetings[i][1] <= 10^9
All start and end times are integers
Meetings may share start or end timesOutput Format
Return a single integer denoting the maximum number of non-overlapping meetings that can be scheduled.
Sample Input 0
1
2
5 10Sample Output 0
1Sample Input 1
3
2
1 2
2 3
3 4Sample Output 1
3Resolution
核心思路: 贪心算法——按结束时间排序,每次选择最早结束且不与已选区间重叠的区间。
为什么按结束时间排序是对的? 结束越早的区间,给后面留下的空间越大,越有机会安排更多区间。这是一个经典的贪心选择性质:局部最优能导出全局最优。
算法步骤:
- 按结束时间升序排序
- 选第一个区间(最早结束的)
- 遍历剩余区间,只要
start >= lastEnd就选,并更新lastEnd
复杂度:
- 时间复杂度:O(n log n),排序占主导
- 空间复杂度:O(1),原地排序无额外空间
同类题: 这道题等价于 LeetCode 435. 无重叠区间(求最多不重叠区间 = 总区间数 - 最少删除区间数)。
我的解法
function maximizeNonOverlappingMeetings(meetings: number[][]): number {
// Write your code here
if (!meetings.length) return 0
meetings.sort((a, b) => a[1] - b[1])
let count = 1
let lastEnd = meetings[0][1]
for (let i = 1; i < meetings.length; i++) {
if (meetings[i][0] >= lastEnd) {
count++
lastEnd = meetings[i][1]
}
}
return count
}思路: 按结束时间排序后,贪心地选择第一个区间,然后遍历后续区间,不重叠就选。
复杂度:
- 时间复杂度:O(n log n),排序占主导,遍历 O(n)
- 空间复杂度:O(1)
优化说明: 循环从 i = 1 开始(第一个区间已默认选中,不需要再和自己比较)。
核心思考:贪心策略的本质
1. 为什么按结束时间排序是对的?
贪心的核心在于**“资源尽早释放”**。 想象时间轴是一条直线,我们要往里塞尽可能多的木块。
- 选结束最早的区间,意味着剩下的可用时间最长(留给后续的选择空间最大)。
- 这不仅仅是直觉,在数学上可以通过**“替换法” (Exchange Argument)** 严格证明:假设存在一个最优解不包含结束最早的区间 A,我们可以把最优解里的第一个区间替换为 A。因为 A 结束得比任何其他区间都早,这种替换绝不会导致后续冲突增加,只会让剩余时间变多。因此,贪心选择永远是安全的。
2. 为什么不按开始时间排序?
开始早不代表“占用少”。
反例: [[1, 100], [2, 3], [4, 5]]
- 按开始时间排:先选
[1, 100],结果后面的[2, 3]和[4, 5]全部因为重叠被跳过。最终只选了 1 个。 - 按结束时间排:先选
[2, 3],再选[4, 5],跳过[1, 100]。最终选了 2 个(最优)。 结论:开始早的区间可能是个“时间黑洞”,直接吃掉了后面所有短区间的生存空间。
3. 举一反三:贪心何时失效?
贪心算法极其高效(O(n log n)),但它只对**“局部最优能推导全局最优”**的问题有效。 如果题目条件稍作修改,贪心就会失效:
- 加权区间调度问题 (Weighted Interval Scheduling):如果每个会议有不同的重要性/价值(比如有的会赚 10 块,有的赚 1000 块),单纯追求“数量最多”或“结束最早”就不够了。此时必须使用动态规划 (DP) 来计算总价值最大化。
- 区间覆盖问题:如果目标是“用最少的区间覆盖整个时间段”,贪心策略会变为“每次选能覆盖当前起点且结束最远的区间”。