Repository navigation
Expand file tree
/
Copy pathmergeIntervals.ts
More file actions
763 lines (669 loc) · 26.1 KB
/
Copy pathmergeIntervals.ts
File metadata and controls
763 lines (669 loc) · 26.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
/**
* ============================================================================
* MERGE INTERVALS PATTERN
* ============================================================================
*
* PATTERN:
* - **Sort by start, then sweep once.** After sorting, any interval can only
* overlap its immediate predecessor's running merge — so one linear pass
* with a "current merged interval" (or a running end/counter) settles
* overlap questions that look pairwise-quadratic.
*
* THE BRUTE-FORCE → OPTIMIZED STORY:
* - Brute force compares every pair of intervals for overlap → O(n²).
* - Sorting imposes an order in which overlaps are ADJACENT: if sorted
* interval i doesn't overlap the merged block before it, nothing later will
* either (starts only increase). That adjacency is what kills the n².
*
* THE TWO CORE CHECKS (memorize these exact comparisons):
* overlap: a.start <= b.end && b.start <= a.end (closed intervals!)
* merge: [min(starts), max(ends)]
* Whether touching endpoints ([1,5],[5,8]) count as overlapping is the #1
* clarifying question to ask — LeetCode's merge (56) says yes, some
* scheduling variants say no.
*
* SUB-FAMILIES IN THIS FILE:
*
* - Merge/insert → merge, insert
* - Intersection sweep → intervalIntersection
* Two sorted lists walked with two pointers.
* - Counting concurrency → canAttendMeetings, minMeetingRooms,
* minPlatformsForTrains. Sort the start times and end times separately,
* then sweep both with a counter — the "chronological ordering" trick.
* - Coverage/removal → removeCoveredIntervals, eraseOverlapIntervals-style
* - Free-time/gaps → employeeFreeTime
*
* RECOGNITION CUES:
* - "meetings / bookings / ranges / schedules" + "merge, conflicts, rooms
* needed, free time" → sort by start (or starts & ends separately) + sweep.
*
* REAL-WORLD ANALOGIES:
* - Calendar consolidation, meeting-room capacity, railway platform planning,
* CIDR/IP-range merging, downtime-window consolidation.
*
* Time: O(n log n) — sorting dominates the O(n) sweep.
* Space: O(1) extra beyond output (or O(n) where a result list is built).
* ============================================================================
*/
// ============================================================================
// INTERVAL DEFINITION
// ============================================================================
export interface Interval {
start: number;
end: number;
}
// ============================================================================
// 1. MERGE INTERVALS
// ============================================================================
/**
* Merge overlapping intervals
*
* Given an array of intervals where intervals[i] = [starti, endi],
* merge all overlapping intervals and return an array of non-overlapping
* intervals that cover all the intervals in the input.
*
* @param intervals - Array of intervals
* @returns Array of merged intervals
*
* Time: O(n log n) - sorting dominates
* Space: O(1) - excluding output array
*
* @example
* // Real-world: Calendar meeting consolidation
* // User has overlapping meetings that need to be merged
* merge([[9, 10], [9.5, 11], [11.5, 12.5], [12, 13]])
* // Returns: [[9, 11], [11.5, 13]]
* // Shows actual busy time blocks: 9-11 AM and 11:30 AM-1 PM
*
* @example
* // Real-world: Time tracking for billing
* // Developer logs work sessions (in hours, some overlap)
* merge([[8, 10], [9.5, 12], [13, 15], [14.5, 16]])
* // Returns: [[8, 12], [13, 16]]
* // Total billable hours: 7 hours (not 8.5)
*
* @example
* // Real-world: Network maintenance windows
* // Different teams schedule overlapping maintenance periods
* merge([[1000, 2000], [1500, 2500], [3000, 4000]])
* // Returns: [[1000, 2500], [3000, 4000]]
* // System knows actual downtime periods
*
* @example
* // Real-world: Resource booking (conference room)
* // Bookings with overlaps due to delays
* merge([[9, 10], [9.45, 11], [11.5, 12.5], [12, 13]])
* // Returns: [[9, 11], [11.5, 13]]
* // Room occupied: 9 AM-11 AM, then 11:30 AM-1 PM
*/
export function merge(intervals: number[][]): number[][] {
// Edge case: No merging needed if 0 or 1 interval
// Example: Single meeting [9, 10] or empty calendar []
if (intervals.length <= 1) return intervals;
// Step 1: Sort by start time (like organizing meetings chronologically)
// Example: [[9.5, 11], [9, 10], [12, 13]] → [[9, 10], [9.5, 11], [12, 13]]
// Why? We need to process meetings in order to detect overlaps
intervals.sort((a, b) => a[0] - b[0]);
// Step 2: Start with the first meeting as our first merged busy period
// Example: First meeting [9, 10] becomes our first busy block
const mergedBusyPeriods: number[][] = [intervals[0]];
// Step 3: Check each subsequent meeting against the last merged period
// Example: Compare [9.5, 11] with [9, 10] to see if they overlap
for (let i = 1; i < intervals.length; i++) {
const currentMeeting = intervals[i]; // e.g., [9.5, 11]
const lastBusyPeriod = mergedBusyPeriods[mergedBusyPeriods.length - 1]; // e.g., [9, 10]
const currentStart = currentMeeting[0]; // e.g., 9.5 (9:30 AM)
const lastEnd = lastBusyPeriod[1]; // e.g., 10 (10:00 AM)
// Check if current meeting overlaps with the last busy period
// Overlap happens when: current meeting starts before/at the last period ends
// Example: [9.5, 11] overlaps [9, 10] because 9.5 <= 10
if (currentStart <= lastEnd) {
// MERGE: Extend the busy period to include both meetings
// Take the later end time to cover both meetings
// Example: [9, 10] + [9.5, 11] → [9, 11] (busy from 9 AM to 11 AM)
const currentEnd = currentMeeting[1]; // e.g., 11
lastBusyPeriod[1] = Math.max(lastEnd, currentEnd);
} else {
// NO OVERLAP: Start a new busy period
// Example: [9, 11] and [12, 13] don't overlap → two separate busy blocks
mergedBusyPeriods.push(currentMeeting);
}
}
// Return the consolidated busy periods
// Example: [[9, 10], [9.5, 11], [12, 13]] → [[9, 11], [12, 13]]
return mergedBusyPeriods;
}
// ============================================================================
// 2. INSERT INTERVAL
// ============================================================================
/**
* Insert a new interval into a sorted list of non-overlapping intervals
*
* @param intervals - Sorted array of non-overlapping intervals
* @param newInterval - New interval to insert
* @returns Array of intervals after insertion
*
* Time: O(n) - single pass through intervals
* Space: O(1) - excluding output array
*/
export function insert(intervals: number[][], newInterval: number[]): number[][] {
const result: number[][] = [];
let i = 0;
// Add all intervals that end before new interval starts
while (i < intervals.length && intervals[i][1] < newInterval[0]) {
result.push(intervals[i]);
i++;
}
// Merge all overlapping intervals
while (i < intervals.length && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
// Add the merged interval
result.push(newInterval);
// Add remaining intervals
while (i < intervals.length) {
result.push(intervals[i]);
i++;
}
return result;
}
// ============================================================================
// 3. INTERVAL INTERSECTION
// ============================================================================
/**
* Find the intersection of two lists of intervals
*
* @param firstList - First list of intervals
* @param secondList - Second list of intervals
* @returns Array of intersection intervals
*
* Time: O(m + n) - where m and n are lengths of the lists
* Space: O(1) - excluding output array
*/
export function intervalIntersection(
firstList: number[][],
secondList: number[][]
): number[][] {
const result: number[][] = [];
let i = 0, j = 0;
while (i < firstList.length && j < secondList.length) {
const first = firstList[i];
const second = secondList[j];
// Find intersection
const start = Math.max(first[0], second[0]);
const end = Math.min(first[1], second[1]);
// If there's an intersection
if (start <= end) {
result.push([start, end]);
}
// Move pointer of interval that ends first
if (first[1] < second[1]) {
i++;
} else {
j++;
}
}
return result;
}
// ============================================================================
// 4. CONFLICTING APPOINTMENTS
// ============================================================================
/**
* Check if a person can attend all appointments
*
* @param intervals - Array of appointment intervals
* @returns True if no conflicts, false otherwise
*
* Time: O(n log n) - sorting dominates
* Space: O(1)
*/
export function canAttendMeetings(intervals: number[][]): boolean {
if (intervals.length <= 1) return true;
// Sort by start time
intervals.sort((a, b) => a[0] - b[0]);
// Check for overlaps
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] < intervals[i - 1][1]) {
return false; // Conflict found
}
}
return true;
}
// ============================================================================
// 5. MINIMUM MEETING ROOMS / MINIMUM PLATFORMS FOR TRAINS
// ============================================================================
/**
* Find the minimum number of meeting rooms required
*
* APPROACH 1: Using Min Heap (Priority Queue)
*
* @param intervals - Array of meeting intervals
* @returns Minimum number of rooms needed
*
* Time: O(n log n) - sorting dominates
* Space: O(n) - for the heap
*/
export function minMeetingRooms(intervals: number[][]): number {
if (intervals.length === 0) return 0;
// Sort intervals by start time
intervals.sort((a, b) => a[0] - b[0]);
// Use a min heap to track end times
const endTimes: number[] = [];
for (const interval of intervals) {
const [start, end] = interval;
// If earliest ending meeting ends before current meeting starts
if (endTimes.length > 0 && endTimes[0] <= start) {
// Remove the earliest ending meeting
endTimes.shift();
}
// Add current meeting's end time
endTimes.push(end);
endTimes.sort((a, b) => a - b); // Keep heap property
}
return endTimes.length;
}
/**
* Find the minimum number of platforms needed for trains
*
* APPROACH 2: Greedy Two-Pointer (More Efficient)
*
* This is the classic "Minimum Platforms" problem:
* Given arrival and departure times of trains, find the minimum number
* of platforms needed so that no train waits.
*
* KEY INSIGHT: We need to find the maximum number of trains present
* at the station at any given time. This is equivalent to finding the
* maximum overlap of intervals.
*
* GREEDY APPROACH:
* 1. Sort both arrival and departure arrays separately
* 2. Use two pointers to traverse both arrays
* 3. When a train arrives (arrival[i] < departure[j]), increment platform count
* 4. When a train departs (arrival[i] >= departure[j]), decrement platform count
* 5. Track the maximum platforms needed at any point
*
* @param arrival - Array of arrival times
* @param departure - Array of departure times
* @returns Minimum number of platforms needed
*
* Time: O(n log n) - sorting dominates
* Space: O(1) - only using a few variables
*
* Example:
* arrival = [1:00, 1:40, 1:50, 2:00, 2:15, 4:00]
* departure = [1:10, 3:00, 2:20, 2:30, 3:15, 6:00]
*
* Converted to minutes for easier comparison:
* arrival = [60, 100, 110, 120, 135, 240]
* departure = [70, 180, 140, 150, 195, 360]
*
* Sorted:
* arrival = [60, 100, 110, 120, 135, 240]
* departure = [70, 140, 150, 180, 195, 360]
*
* Timeline:
* 60: Train 1 arrives → platforms = 1, max = 1
* 70: Train 1 departs → platforms = 0, max = 1
* 100: Train 2 arrives → platforms = 1, max = 1
* 110: Train 3 arrives → platforms = 2, max = 2
* 120: Train 4 arrives → platforms = 3, max = 3
* 135: Train 5 arrives → platforms = 4, max = 4
* 140: Train 3 departs → platforms = 3, max = 4
* 150: Train 4 departs → platforms = 2, max = 4
* 180: Train 2 departs → platforms = 1, max = 4
* 195: Train 5 departs → platforms = 0, max = 4
* 240: Train 6 arrives → platforms = 1, max = 4
* 360: Train 6 departs → platforms = 0, max = 4
*
* Answer: 4 platforms
*/
export function minPlatformsForTrains(
arrival: number[],
departure: number[]
): number {
if (arrival.length === 0) return 0;
if (arrival.length !== departure.length) {
throw new Error('Arrival and departure arrays must have the same length');
}
const n = arrival.length;
// Sort both arrays separately
// This is the key: we need to process events in chronological order
const sortedArrival = [...arrival].sort((a, b) => a - b);
const sortedDeparture = [...departure].sort((a, b) => a - b);
// Two pointers: i for arrival, j for departure
let i = 0; // pointer for arrival array
let j = 0; // pointer for departure array
let platformsNeeded = 0; // current number of platforms in use
let maxPlatforms = 0; // maximum platforms needed at any time
// Process all events (arrivals and departures)
while (i < n && j < n) {
// If arrival happens before departure, a train arrives
if (sortedArrival[i] < sortedDeparture[j]) {
platformsNeeded++; // need one more platform
i++; // move to next arrival
// Update maximum if needed
if (platformsNeeded > maxPlatforms) {
maxPlatforms = platformsNeeded;
}
} else {
// Departure happens (arrival >= departure)
// A train leaves, so we free up a platform
platformsNeeded--; // free one platform
j++; // move to next departure
}
}
// Note: After the loop, if i < n, there are more arrivals
// but we've already found the maximum, so we don't need to process them
return maxPlatforms;
}
/**
* Alternative: Minimum Platforms using intervals (same as minMeetingRooms)
*
* This converts the arrival/departure arrays into intervals and uses
* the same logic as minMeetingRooms.
*
* @param arrival - Array of arrival times
* @param departure - Array of departure times
* @returns Minimum number of platforms needed
*/
export function minPlatformsUsingIntervals(
arrival: number[],
departure: number[]
): number {
if (arrival.length === 0) return 0;
// Convert to intervals
const intervals: number[][] = [];
for (let i = 0; i < arrival.length; i++) {
intervals.push([arrival[i], departure[i]]);
}
// Use the same logic as minMeetingRooms
return minMeetingRooms(intervals);
}
// ============================================================================
// 6. EMPLOYEE FREE TIME
// ============================================================================
/**
* Find free time intervals for all employees
*
* @param schedule - Array of employee schedules
* @returns Array of free time intervals
*
* Time: O(n log n) - where n is total number of intervals
* Space: O(n) - for merged intervals
*/
export function employeeFreeTime(schedule: number[][][]): number[][] {
// Flatten all intervals
const allIntervals: number[][] = [];
for (const employee of schedule) {
allIntervals.push(...employee);
}
// Sort by start time
allIntervals.sort((a, b) => a[0] - b[0]);
// Merge overlapping intervals
const merged = merge(allIntervals);
// Find gaps between merged intervals
const freeTime: number[][] = [];
for (let i = 1; i < merged.length; i++) {
const gapStart = merged[i - 1][1];
const gapEnd = merged[i][0];
if (gapStart < gapEnd) {
freeTime.push([gapStart, gapEnd]);
}
}
return freeTime;
}
// ============================================================================
// 7. REMOVE COVERED INTERVALS
// ============================================================================
/**
* Remove intervals that are covered by another interval
*
* An interval [a, b) is covered by another interval [c, d) if c <= a and b <= d.
*
* @param intervals - Array of intervals
* @returns Number of intervals after removing covered ones
*
* Time: O(n log n) - sorting dominates
* Space: O(1)
*/
export function removeCoveredIntervals(intervals: number[][]): number {
if (intervals.length <= 1) return intervals.length;
// Sort by start time, then by end time (descending)
intervals.sort((a, b) => {
if (a[0] !== b[0]) {
return a[0] - b[0];
}
return b[1] - a[1]; // Descending end time
});
let count = 1;
let prevEnd = intervals[0][1];
for (let i = 1; i < intervals.length; i++) {
const [, end] = intervals[i]; // start not needed — the sort already ordered by it
// If current interval is not covered
if (end > prevEnd) {
count++;
prevEnd = end;
}
}
return count;
}
// ============================================================================
// 8. PARTITION LABELS
// ============================================================================
/**
* Partition a string into as many parts as possible so that each letter
* appears in at most one part.
*
* @param s - Input string
* @returns Array of partition sizes
*
* Time: O(n) - single pass through string
* Space: O(1) - constant extra space
*/
export function partitionLabels(s: string): number[] {
// Find last occurrence of each character
const lastOccurrence: { [key: string]: number } = {};
for (let i = 0; i < s.length; i++) {
lastOccurrence[s[i]] = i;
}
const result: number[] = [];
let start = 0;
let end = 0;
for (let i = 0; i < s.length; i++) {
// Extend the partition to include the last occurrence of current character
end = Math.max(end, lastOccurrence[s[i]]);
// If we've reached the end of current partition
if (i === end) {
result.push(end - start + 1);
start = i + 1;
}
}
return result;
}
// ============================================================================
// 9. INTERVAL LIST INTERSECTIONS
// ============================================================================
/**
* Find all intersections between two lists of intervals
*
* @param firstList - First list of intervals
* @param secondList - Second list of intervals
* @returns Array of all intersection intervals
*
* Time: O(m + n) - where m and n are lengths of the lists
* Space: O(1) - excluding output array
*/
export function intervalListIntersections(
firstList: number[][],
secondList: number[][]
): number[][] {
const result: number[][] = [];
let i = 0, j = 0;
while (i < firstList.length && j < secondList.length) {
const first = firstList[i];
const second = secondList[j];
// Find intersection
const start = Math.max(first[0], second[0]);
const end = Math.min(first[1], second[1]);
// If there's an intersection
if (start <= end) {
result.push([start, end]);
}
// Move pointer of interval that ends first
if (first[1] < second[1]) {
i++;
} else {
j++;
}
}
return result;
}
// ============================================================================
// 10. MINIMUM ARROW TO BURST BALLOONS
// ============================================================================
/**
* Find minimum number of arrows to burst all balloons
*
* @param points - Array of balloon intervals
* @returns Minimum number of arrows needed
*
* Time: O(n log n) - sorting dominates
* Space: O(1)
*/
export function findMinArrowShots(points: number[][]): number {
if (points.length === 0) return 0;
// Sort by end position
points.sort((a, b) => a[1] - b[1]);
let arrows = 1;
let end = points[0][1];
for (let i = 1; i < points.length; i++) {
// If current balloon starts after previous arrow position
if (points[i][0] > end) {
arrows++;
end = points[i][1];
}
}
return arrows;
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Check if two intervals overlap
*/
export function intervalsOverlap(interval1: number[], interval2: number[]): boolean {
return interval1[0] <= interval2[1] && interval2[0] <= interval1[1];
}
/**
* Merge two overlapping intervals
*/
export function mergeTwoIntervals(interval1: number[], interval2: number[]): number[] {
return [
Math.min(interval1[0], interval2[0]),
Math.max(interval1[1], interval2[1])
];
}
/**
* Check if an interval is covered by another
*/
export function isIntervalCovered(covered: number[], covering: number[]): boolean {
return covering[0] <= covered[0] && covered[1] <= covering[1];
}
/**
* Sort intervals by start time
*/
export function sortIntervalsByStart(intervals: number[][]): number[][] {
return intervals.sort((a, b) => a[0] - b[0]);
}
/**
* Sort intervals by end time
*/
export function sortIntervalsByEnd(intervals: number[][]): number[][] {
return intervals.sort((a, b) => a[1] - b[1]);
}
/**
* ============================================================================
* NON-OVERLAPPING INTERVALS (LeetCode 435) — THE "KEEP THE MOST" GREEDY
* ============================================================================
* PROBLEM: return the MINIMUM number of intervals to remove so that the rest
* do not overlap.
*
* ⭐ FIRST, FLIP THE QUESTION. "Fewest to remove" is a subtraction away from
* "MOST to keep", and the keep-version has a clean greedy answer while the
* remove-version does not. Restating a minimisation as the complementary
* maximisation is the move worth taking from this problem:
*
* answer = total − (largest non-overlapping subset)
*
* ============================================================================
* 🔑 SORT BY END, NOT BY START — and here is the proof sketch
* ============================================================================
* This is the "activity selection" greedy. Sort by END and always keep the
* interval that finishes EARLIEST among those that still fit.
*
* WHY EARLIEST-END IS OPTIMAL: whichever interval you keep, all it does for
* the rest of the problem is block everything up to its end. So the interval
* that ends soonest blocks the least, leaving the most room for later choices.
* No other single choice can do better, and swapping any optimal solution's
* first interval for the earliest-ending one keeps it valid and the same size.
*
* ⚠️ SORTING BY START FAILS, and it is worth being able to show it instantly:
*
* intervals = [[1,100], [2,3], [3,4]]
* by START : keep [1,100] first → it blocks everything → keep 1, remove 2 ✗
* by END : keep [2,3], then [3,4] → keep 2, remove 1 ✓ (the answer)
*
* That one example is the whole reason the package header says the sort key
* IS the problem: by start to BUILD a combined range (`merge`, `insert`), by
* end to KEEP as many as possible (here, and `findMinArrowShots`).
*
* ============================================================================
* 🔗 COMPARE WITH findMinArrowShots (LC452), just above
* ============================================================================
* Identical greedy, one character apart:
* - LC452 (arrows): intervals are CLOSED, so touching balloons ([1,2],[2,3])
* share an arrow → the "conflict" test is `start > lastEnd`.
* - LC435 (here): intervals are HALF-OPEN, so touching intervals do NOT
* overlap and both are kept → the test is `start >= lastEnd`.
* Being able to say which comparison each problem needs, and why, is the
* difference between having memorised one and understanding both.
*
* DRY-RUN on [[1,2], [2,3], [3,4], [1,3]]:
* sorted by end: [1,2], [2,3], [1,3], [3,4]
* keep [1,2] kept = 1, lastEnd = 2
* [2,3]: start 2 >= 2 → keep kept = 2, lastEnd = 3
* [1,3]: start 1 < 3 → overlaps, skip
* [3,4]: start 3 >= 3 → keep kept = 3, lastEnd = 4
* removals = 4 - 3 = 1 ✓
*
* @example
* eraseOverlapIntervals([[1,2],[2,3],[3,4],[1,3]]); // 1
* eraseOverlapIntervals([[1,2],[1,2],[1,2]]); // 2 (keep one of three)
* eraseOverlapIntervals([[1,2],[2,3]]); // 0 (touching is fine)
* eraseOverlapIntervals([]); // 0
*
* Time: O(n log n) — dominated by the sort. Space: O(n) for the sorted copy.
*/
export function eraseOverlapIntervals(intervals: number[][]): number {
if (intervals.length <= 1) return 0;
// Copy before sorting: Array.prototype.sort mutates in place, and a
// function that silently reorders its caller's data is a nasty surprise.
const sorted = [...intervals].sort((a, b) => a[1] - b[1]); // by END
let kept = 1;
let lastEnd = sorted[0][1];
for (let i = 1; i < sorted.length; i++) {
const [start, end] = sorted[i];
// >= because touching intervals do NOT overlap here (half-open).
// Contrast findMinArrowShots, which uses > for closed intervals.
if (start >= lastEnd) {
kept++;
lastEnd = end;
}
// else: this interval overlaps the one we kept — drop it. No need to
// update lastEnd, because sorting by end means the kept one already
// finishes no later than this one.
}
return intervals.length - kept; // flip back to "how many removed"
}