0Pricing
Coding Interview Prep · Урок

Планирование и объединение интервалов

Решите задачи о переговорных комнатах и непересекающихся интервалах, сортируя интервалы по времени окончания, а интервалы для объединения — по времени начала

«Планирование и объединение интервалов» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Жадные алгоритмы и DP: когда что использовать
  2. Планирование и объединение интервалов
  3. Игра с прыжками I и II
  4. Планировщик задач и заправочная станция
← Назад к Coding Interview Prep