Планирование и объединение интервалов
Решите задачи о переговорных комнатах и непересекающихся интервалах, сортируя интервалы по времени окончания, а интервалы для объединения — по времени начала
«Планирование и объединение интервалов» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Обзор задач на интервалы
Задачи на интервалы постоянно встречаются на собеседованиях по планированию, управлению календарём и распределению ресурсов. Ключевые подходы: объединение пересекающихся интервалов, подсчёт минимального числа комнат для встреч, поиск максимального множества непересекающихся интервалов и вставка нового интервала. Большинство задач на интервалы начинаются с одного и того же шага: выполните sort интервалов по времени начала (или по времени окончания — в зависимости от задачи). Правильный выбор ключа сортировки часто оказывается самой сложной частью.
# Intervals: each = [start, end] (inclusive or exclusive by problem)
# Example:
intervals = [[1,3],[2,6],[8,10],[15,18]]
# Sorted by start (already sorted here)
# Visually:
# [1,3] |-|
# [2,6] |---|
# [8,10] |--|
# [15,18] |---|
print('Intervals ready for analysis')Объединение пересекающихся интервалов
Объединение интервалов (LeetCode 56): дан список интервалов, объедините все пересекающиеся. Алгоритм: выполните sort по времени начала. Пройдите по отсортированному списку; если текущий интервал пересекается с последним объединённым интервалом (его начало ≤ end последнего объединённого), увеличьте end последнего объединённого интервала до максимума из двух концов. В противном случае append текущий интервал как новый объединённый интервал. Время выполнения: O(n log n) для сортировки и O(n) для объединения.
def merge_intervals(intervals):
intervals.sort(key=lambda x: x[0]) # sort by start
merged = [intervals[0]]
for start, end in intervals[1:]:
last_end = merged[-1][1]
if start <= last_end:
# Overlapping: extend the last interval
merged[-1][1] = max(last_end, end)
else:
# Non-overlapping: add as new interval
merged.append([start, end])
return merged
print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# [[1,6],[8,10],[15,18]]
print(merge_intervals([[1,4],[4,5]]))
# [[1,5]] (touching intervals merge)Вставка интервала
Вставка интервала (LeetCode 57): дан отсортированный список непересекающихся интервалов; вставьте новый интервал и снова объедините пересечения. Пройдите три этапа: (1) Добавьте все интервалы, которые заканчиваются до начала нового интервала. (2) Объедините все интервалы, пересекающиеся с новым интервалом (расширьте его границы). (3) Добавьте все оставшиеся интервалы. Это один проход за O(n) после сортировки за O(n log n), которая в этой задаче уже выполнена.
def insert_interval(intervals, new_interval):
result = []
i = 0
n = len(intervals)
# Phase 1: intervals before new_interval
while i < n and intervals[i][1] < new_interval[0]:
result.append(intervals[i])
i += 1
# Phase 2: merge overlapping intervals
while i < n and intervals[i][0] <= new_interval[1]:
new_interval[0] = min(new_interval[0], intervals[i][0])
new_interval[1] = max(new_interval[1], intervals[i][1])
i += 1
result.append(new_interval)
# Phase 3: remaining intervals
while i < n:
result.append(intervals[i])
i += 1
return result
print(insert_interval([[1,3],[6,9]], [2,5])) # [[1,5],[6,9]]
print(insert_interval([[1,2],[3,5],[6,7],[8,10],[12,16]], [4,8]))
# [[1,2],[3,10],[12,16]]Комнаты для встреч I: можно ли посетить все встречи
Комнаты для встреч I (LeetCode 252): даны интервалы времени встреч, определите, может ли человек посетить все встречи. Выполните sort по времени начала; если какая-либо встреча начинается до окончания предыдущей, они пересекаются. Это самая простая проверка интервалов — общая сложность O(n log n). Ключевая идея: после сортировки нужно сравнивать только соседние пары.
def can_attend_meetings(intervals):
intervals.sort(key=lambda x: x[0])
for i in range(1, len(intervals)):
# Current meeting starts before previous ends?
if intervals[i][0] < intervals[i-1][1]:
return False
return True
print(can_attend_meetings([[0,30],[5,10],[15,20]])) # False (0,30 overlaps 5,10)
print(can_attend_meetings([[7,10],[2,4]])) # True (4 < 7, no overlap)Комнаты для встреч II: минимальное число комнат
Комнаты для встреч II (LeetCode 253): найдите минимальное число конференц-залов, необходимое для одновременного проведения всех встреч. Используйте минимальную кучу, чтобы отслеживать зал, освобождающийся раньше всего. Отсортируйте встречи по времени начала. Для каждой новой встречи: если она начинается после end time зала, освобождающегося раньше всего, повторно используйте этот зал (выполните pop и добавьте встречу). В противном случае откройте новый зал. Размер кучи в конце равен необходимому числу залов.
import heapq
def min_meeting_rooms(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[0]) # sort by start
heap = [] # min-heap of end times
for start, end in intervals:
if heap and heap[0] <= start:
heapq.heapreplace(heap, end) # reuse earliest-ending room
else:
heapq.heappush(heap, end) # open a new room
return len(heap)
print(min_meeting_rooms([[0,30],[5,10],[15,20]])) # 2
print(min_meeting_rooms([[7,10],[2,4]])) # 1
print(min_meeting_rooms([[9,10],[4,9],[4,17]])) # 2Альтернативный метод подсчёта комнат с помощью сканирующей прямой
Альтернативный подход со сложностью O(n log n): сканирующая прямая. Создайте события для начала (+1) и конца (-1) каждого интервала. Отсортируйте все события по time (при совпадении: конец раньше начала, если границы не включаются). Двигайтесь слева направо, поддерживая текущее число активных встреч. Максимальное число встреч равно минимальному числу необходимых комнат. Для некоторых задач этот подход интуитивнее, а также его можно обобщить на другие задачи подсчёта для интервалов.
def min_rooms_sweep(intervals):
events = []
for start, end in intervals:
events.append((start, 1)) # meeting starts
events.append((end, -1)) # meeting ends
# Sort: same time → end (-1) before start (1) if exclusive
events.sort(key=lambda x: (x[0], x[1]))
max_rooms = current = 0
for _, delta in events:
current += delta
max_rooms = max(max_rooms, current)
return max_rooms
print(min_rooms_sweep([[0,30],[5,10],[15,20]])) # 2
print(min_rooms_sweep([[1,5],[2,6],[3,7]])) # 3 (all overlap at t=3)Непересекающиеся интервалы: максимальный выбор
Непересекающиеся интервалы (LeetCode 435): найдите минимальное число интервалов, которые нужно удалить, чтобы остальные не пересекались. Это эквивалентно поиску максимального числа непересекающихся интервалов (выбор действий) с последующим удалением остальных. Выполните sort по end time: жадно оставляйте интервал, который заканчивается раньше всего (он оставляет максимум пространства для будущих интервалов). Если следующий интервал пересекается с выбранным, выполните discard (увеличьте счётчик удалений).
def erase_overlap_intervals(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[1]) # sort by END time
removals = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
last_end = end # keep this interval
else:
removals += 1 # remove this interval (it overlaps)
return removals
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]])) # 1 (remove [1,3])
print(erase_overlap_intervals([[1,2],[1,2],[1,2]])) # 2
print(erase_overlap_intervals([[1,2],[2,3]])) # 0 (no overlap)Почему сортировать по времени окончания, а не начала?
Для задачи выбора занятий (максимального множества непересекающихся занятий) доказано, что сортировка по времени окончания оптимальна. Интуитивно: занятие, которое заканчивается раньше, оставляет больше места для будущих занятий. Если сортировать по времени начала, можно выбрать очень длинное занятие, начинающееся рано, и заблокировать множество более коротких занятий, начинающихся позже. Обменный аргумент: если оптимальное решение выбирает занятие A вместо заканчивающегося раньше всех G, заменим A на G — G заканчивается не позже, поэтому оно не конфликтует ни с одним занятием, с которым не конфликтовало A.
# Counterexample for sorting by START time:
# [[1,10],[2,3],[4,5]] — sorted by start: [1,10],[2,3],[4,5]
# Sort-by-start greedy keeps [1,10], can't add [2,3] or [4,5] (all overlap [1,10])
# Selects: 1 interval
# Sort-by-end greedy:
# [[2,3],[4,5],[1,10]] — sorted by end
# Keep [2,3] (end=3), then [4,5] (start=4 >= 3, keep), then [1,10] (start=1 < 5, skip)
# Selects: 2 intervals — OPTIMAL
intervals = [[1,10],[2,3],[4,5]]
intervals.sort(key=lambda x: x[1])
last_end = float('-inf')
count = 0
for s, e in intervals:
if s >= last_end:
count += 1; last_end = e
print('Max non-overlapping:', count) # 2Пересечения списков интервалов
Пересечения списков интервалов (LeetCode 986): найдите все пересекающиеся пары в двух отсортированных списках интервалов. Используйте подход с двумя указателями. На каждом шаге вычисляйте пересечение текущей пары: максимум начал и минимум концов. Если начало ≤ конец, пересечение действительно. Затем переместите указатель того интервала, который заканчивается раньше. Временная сложность — O(m+n).
def interval_intersection(A, B):
result = []
i = j = 0
while i < len(A) and j < len(B):
# Intersection boundaries
lo = max(A[i][0], B[j][0])
hi = min(A[i][1], B[j][1])
if lo <= hi:
result.append([lo, hi]) # valid intersection
# Advance pointer of interval that ends first
if A[i][1] < B[j][1]:
i += 1
else:
j += 1
return result
A = [[0,2],[5,10],[13,23],[24,25]]
B = [[1,5],[8,12],[15,24],[25,26]]
print(interval_intersection(A, B))
# [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]Разбиение на части
Разбиение на части (LeetCode 763): разделите строку на максимально возможное число частей так, чтобы каждый символ встречался не более чем в одной части. Жадный подход: для каждого символа найдите его последнее вхождение. Пройдите по строке, поддерживая значение max_end. Когда i == max_end, текущая часть завершена — запишите её длину и начните новую часть. По сути, это замаскированная задача на объединение интервалов.
def partition_labels(s):
last = {c: i for i, c in enumerate(s)} # last occurrence of each char
partitions = []
start = max_end = 0
for i, c in enumerate(s):
max_end = max(max_end, last[c])
if i == max_end: # partition complete
partitions.append(max_end - start + 1)
start = i + 1
return partitions
print(partition_labels('ababcbacadefegdehijhklij'))
# [9, 7, 8] — parts 'ababcbaca', 'defegde', 'hijhklij'Итоги по задачам на интервалы
Освойте эти четыре шаблона для работы с интервалами: (1) объединение: отсортируйте по началу и расширяйте последний интервал при пересечении; (2) подсчёт комнат: отсортируйте по началу и используйте минимальную кучу концов интервалов; (3) максимальное число непересекающихся интервалов: отсортируйте по концу и выполняйте жадный выбор; (4) вставка: линейный просмотр в три этапа. Ключ sort имеет значение: объединение использует начало, а выбор максимума — конец. Временная сложность всегда O(n log n), причём основную часть определяет операция sort; объединение и просмотр выполняются за O(n).
# Quick reference:
# Merge intervals: sort by start, extend if overlap
# Insert interval: three-phase linear scan
# Meeting rooms (can?): sort by start, check consecutive overlap
# Meeting rooms (min?): sort by start, min-heap of end times / sweep
# Max non-overlapping: sort by END, greedy keep
# Min removals: n - max_non_overlapping
# Interval intersection: two pointers on sorted lists
print('Pattern: sort key is the decisive choice')
print('Merge → sort by start')
print('Activity selection → sort by end')
print('Room count → sort by start + heap of ends')Быстрая проверка
Проверьте, насколько хорошо Вы поняли концепции «Структуры данных & алгоритмы — подготовка к собеседованию по программированию» из этого урока.
Итоги урока
В этом уроке Вы узнали: как объединять интервалы, сортируя их по началу и расширяя последний интервал при пересечении, как находить минимальное число комнат для встреч с помощью сортировки по началу и минимальной кучи концов интервалов, повторно используя комнату, когда комната с самым ранним окончанием освобождается, а также как находить максимальное число непересекающихся интервалов с помощью жадного выбора по времени окончания. Далее мы рассмотрим Игру с прыжками I и II — задачи на достижимость и минимальное число прыжков, решаемые с помощью жадного расширения диапазона.
Часто задаваемые вопросы
Урок «Планирование и объединение интервалов» бесплатный?
Да — полный текст урока «Планирование и объединение интервалов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Планирование и объединение интервалов»?
Решите задачи о переговорных комнатах и непересекающихся интервалах, сортируя интервалы по времени окончания, а интервалы для объединения — по времени начала Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Планирование и объединение интервалов»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Жадные алгоритмы и DP: когда что использовать
- Планирование и объединение интервалов
- Игра с прыжками I и II
- Планировщик задач и заправочная станция