К этапу 7

Этап 7 · урок 3

Интервалы, пересечения и объединение

После сортировки по началу достаточно сравнивать новый интервал с последним уже объединённым.

Язык кода

После урока вы сможете

  • формулировать условие пересечения интервалов
  • объединять интервалы одним проходом после сортировки

После сортировки [1,3],[2,6],[8,10] второй интервал пересекает хвост ответа и расширяет его до [1,6]; [8,10] начинается позже 6 и создаёт новый блок.

Как объединить пересекающиеся интервалы?

Объединить все пересекающиеся закрытые интервалы [start,end].

Поиск и слияние произвольной пары

Пока возможно, искать любую пересекающуюся пару, объединять её и начинать поиск заново.

Почему повторный поиск пересечений запутывает решение?

После каждого объединения пары пересматриваются, что может привести к O(n²) и сложной логике.

Сортировка по start оставляет один активный хвост

После сортировки по start следующий интервал либо пересекается с последним объединённым, либо начинает новую отдельную группу.

Что гарантирует массив merged после каждого интервала?

merged содержит точное объединение обработанного префикса, интервалы в нём отсортированы и не пересекаются.

Расширяем последний блок или начинаем новый

Отсортировать интервалы. Первый добавить в ответ. Для каждого следующего: если start <= merged.back.end, расширить end максимумом; иначе добавить новый.

Слияние интервалов на C++17 и Python 3

C++17

#include <algorithm>
#include <cassert>
#include <stdexcept>
#include <vector>
using namespace std;

struct Interval {
    int start;
    int end;
};

vector<Interval> mergeIntervals(vector<Interval> intervals) {
    if (intervals.empty()) return {};
    for (const Interval& interval : intervals) {
        if (interval.start > interval.end) {
            throw invalid_argument("start exceeds end");
        }
    }
    sort(intervals.begin(), intervals.end(), [](Interval a, Interval b) {
        return a.start < b.start || (a.start == b.start && a.end < b.end);
    });
    vector<Interval> merged{intervals[0]};
    for (int i = 1; i < static_cast<int>(intervals.size()); ++i) {
        if (intervals[i].start <= merged.back().end) {
            merged.back().end = max(merged.back().end, intervals[i].end);
        } else {
            merged.push_back(intervals[i]);
        }
    }
    return merged;
}

int main() {
    auto merged = mergeIntervals({{1, 3}, {2, 6}, {8, 10}});
    assert(merged.size() == 2);
    assert(merged[0].start == 1 && merged[0].end == 6);
    assert(merged[1].start == 8 && merged[1].end == 10);
}

Python 3

def merge_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]:
    if not intervals:
        return []
    if any(start > end for start, end in intervals):
        raise ValueError("start exceeds end")
    ordered = sorted(intervals)
    merged = [ordered[0]]
    for start, end in ordered[1:]:
        last_start, last_end = merged[-1]
        if start <= last_end:
            merged[-1] = (last_start, max(last_end, end))
        else:
            merged.append((start, end))
    return merged


assert merge_intervals([(1, 3), (2, 6), (8, 10)]) == [(1, 6), (8, 10)]
assert merge_intervals([]) == []

Сортировка доминирует над линейным проходом

O(n log n) времени из-за сортировки и O(n) памяти для ответа. Сам проход — O(n).

Пустой ввод, касание границ и вложенные интервалы

Пустой ввод; касание [1,2] и [2,3] для закрытых интервалов объединяется; вложенные интервалы; одинаковые начала. Контракт требует start <= end, иначе реализация отклоняет интервал.

Набор тестов на все виды пересечения

[], [[1,3],[2,6],[8,10]] -> [[1,6],[8,10]], вложение, касание границ, ошибочный [5,2].

Как распознать задачу о временных диапазонах?

Объекты имеют начало и конец, требуется объединение, покрытие или выбор непересекающихся объектов.

Когда закрытые и полуоткрытые интервалы нельзя смешивать?

Условие пересечения зависит от модели: полуинтервалы [l,r) при next.start == current.end не пересекаются.

Мини-проверка: почему достаточно сравнивать с последним интервалом?

Вопрос: почему не нужно сравнивать со всеми интервалами ответа? Ответ: они не пересекаются и отсортированы; пересечься может только последний.

Мини-проверка: зачем брать максимум правых границ?

Вопрос: почему end обновляется максимумом? Ответ: вложенный интервал не должен уменьшить уже покрытую правую границу.

Объедините три диапазона вручную

Объедините [1,4],[0,2],[3,5],[8,9], показывая состояние merged после каждого отсортированного интервала.

Нормализуйте расписание без подсказки

Задача 1

Объедините полуинтервалы [l,r), где касание границ не считается пересечением, и сохраните результат отсортированным.

После сортировки важен только последний объединённый блок

Сортировка превращает глобальную задачу пересечений в локальное сравнение с последним объединённым интервалом.

Не начат