6. Meeting Rooms
«Один человек не может находиться в двух местах одновременно — это простейший инвариант материального мира».
— Брайан Керниган
Задача Meeting Rooms (LeetCode 252, Easy) моделирует фундаментальный инвариант физического мира и системного программирования: эксклюзивное владение неразделяемым ресурсом.
Условие задачи:
Дан срез интервалов встреч
intervals, гдеintervals[i] = [start, end].
Определите, может ли один человек посетить все запланированные встречи без временных коллизий (наложений).
Если одна встреча заканчивается в момент времени $T$, а следующая начинается ровно в $T$, такое касание не является конфликтом.
В архитектуре бэкенда это прямая модель проверки эксклюзивных строковых блокировок (SELECT ... FOR UPDATE), валидации пользовательских тайм-слотов в системах бронирования и детекции конфликтов расписания в планировщиках cron-задач.
1. Алгоритмический анализ: Квадратичный перебор против Сортировки
Наивный подход за $\mathcal{O}(N^2)$
Если оставить интервалы в произвольном порядке, нам придется сравнивать каждую встречу со всеми остальными через двойной цикл:
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if max(intervals[i][0], intervals[j][0]) < min(intervals[i][1], intervals[j][1]) {
return false // Найдено пересечение
}
}
}
При $N = 10\,000$ это потребует $5 \cdot 10^7$ проверок и неприемлемо для боевого сервиса.
Оптимальный инвариант сортировки
Если упорядочить все встречи по времени их начала (Start), то любой возможный конфликт во времени гарантированно локализуется между двумя строго соседними элементами:
$$\text{intervals}[i][0] < \text{intervals}[i-1][1]$$
Если соседние встречи не пересекаются, то никакие другие пересечься уже физически не способны!
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef sort fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef loop fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef check fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:2px,color:#f8fafc
In["Вход: intervals [][]int"]:::entry --> S["Сортировка по Start:<br>slices.SortFunc за O(N log N)"]:::sort
S --> L["Итерация i от 1 до N-1"]:::loop
L --> C{"intervals[i][0] < intervals[i-1][1]?"}:::check
C -- "Да (Конфликт!)" --> Fail["false (Ранний выход)"]:::error
C -- "Нет (Совместимы)" --> Nxt{"Есть еще встречи?"}:::loop
Nxt -- "Да" --> L
Nxt -- "Нет" --> Ok["true (Все встречи совместимы)"]:::success
2. Идиоматичная реализация на Go 1.21+
package main
import "slices"
// CanAttendMeetings возвращает true, если человек может посетить все встречи.
// Временная сложность: O(N log N) — сортировка.
// Память: O(1) extra space — сортировка выполняется in-place.
func CanAttendMeetings(intervals [][]int) bool {
if len(intervals) <= 1 {
return true
}
// 1. Сортируем встречи по времени начала
slices.SortFunc(intervals, func(a, b []int) int {
return a[0] - b[0]
})
// 2. Линейная проверка смежных соседей с ранним выходом
for i := 1; i < len(intervals); i++ {
if intervals[i][0] < intervals[i-1][1] {
return false // Обнаружено перекрытие во времени
}
}
return true
}
3. Mechanical Sympathy: Early Exit и Branch Target Buffer
- Ранний выход (Early Exit):
В отличие от задач полного слияния, здесь при обнаружении первого же пересечения функция немедленно завершает работу (return false). В реальных производственных календарях с высокой плотностью записей конфликт обычно обнаруживается в первых 5–10% массива, что снижает среднее время выполнения до долей микросекунды. - Предсказуемость ветвлений:
Если конфликтов нет (лучший сценарий,return true), проверкаintervals[i][0] < intervals[i-1][1]дает монотонный результатfalse$N-1$ раз подряд. Блок предсказания ветвлений процессора (Branch Target Buffer) фиксирует этот паттерн, обеспечивая конвейеру CPU непрерывную работу без сбросов. - Кэш-локальность:
Сравнениеintervals[i]иintervals[i-1]обращается к смежным адресам памяти. Аппаратный блок упреждающей выборки (Hardware Prefetcher) подгружает строки L1-кэша заранее.
4. Граничные случаи (Edge Cases)
- Граничные касания (
[1, 5]и[5, 10]):
Строгое неравенство5 < 5ложно, конфликта нет, возвращаетсяtrue. - Мгновенные встречи нулевой длительности (
[4, 4]):
Если в расписании есть звонок[4, 4]и встреча[4, 5], проверка4 < 4ложна. Касание в точке допустимо. - Пустой срез или срез из 1 встречи:
Конфликты невозможны, guard clause мгновенно возвращаетtrue.
5. Вопросы на собеседовании
6. Резюме
- Сортировка локализует коллизии: После сортировки по
Startдостаточно проверять только соседние парыintervals[i]иintervals[i-1]. - Строгое неравенство: Условие
curr[0] < prev[1]корректно обрабатывает точечные стыки встреч. - Zero Allocations: Алгоритм работает in-place без создания новых срезов.
В следующей статье мы масштабируем эту задачу от одного человека до целого офиса и научимся находить минимальное количество переговорных комнат для проведения всех встреч: 7. Meeting Rooms II.