К содержанию

Подтема 5 из 7

Интервалы и события

Мероприятия по раннему окончанию и второй критерий последнего, объединение интервалов и простои, события нагрузки и окно наблюдения.

Раннее окончание

В зал подали заявки на мероприятия (1, 3), (3, 5), (5, 8), (9, 15), (12, 16). Как провести как можно больше мероприятий, чтобы они не пересекались?

Каждый раз брать то мероприятие, которое раньше заканчивается: оно раньше освобождает зал.

У мероприятия есть начало и окончание: (start, finish). Пусть следующее мероприятие разрешено начать в момент окончания предыдущего. Тогда мероприятия совместимы, если start следующего ≥ finish предыдущего, а если нужен перерыв P — если start следующего ≥ finish предыдущего + P.

На рисунке видно, как зал освобождается раньше всего и для следующих мероприятий остаётся больше времени:

Выбраны (1, 3), (3, 5), (5, 8) и (9, 15) — четыре мероприятия; (12, 16) начинается раньше окончания (9, 15) и не помещается.
meetings = [(9, 15), (1, 3), (12, 16), (5, 8), (3, 5)]pause = 0meetings.sort(key=lambda x: (x[1], x[0]))selected = []last_finish = Nonefor start, finish in meetings:    if last_finish is None or start >= last_finish + pause:        selected.append((start, finish))        last_finish = finishprint(len(selected), *selected)
4 (1, 3) (3, 5) (5, 8) (9, 15)

Первый показатель ключа — окончание. Почему первый выбор обрабатывается через ? Чтобы случайно не потребовать перерыв до первого мероприятия: перерыв по условию нужен между мероприятиями.

Обоснование · необязательноДоказательство выбора обменом

Пусть некоторое оптимальное расписание начинается с мероприятия A. Наш алгоритм выбирает мероприятие G, заканчивающееся не позже A. Заменим A на G. Все остальные мероприятия расписания останутся допустимыми: зал освободится не позже прежнего. Количество мероприятий не уменьшилось. После первого выбора рассуждение повторяется для оставшейся части дня.

Почему не по началу

А почему не брать мероприятие, которое раньше начинается, или самое короткое?

Оба правила ошибаются — это видно на маленьких примерах.

Почему не по началу? Раннее мероприятие может занять весь день:

По началу выбирается (0, 20) — одно мероприятие, хотя можно провести три: (1, 3), (3, 5), (5, 7).

Почему не по длительности? Короткое мероприятие может пересекаться с двумя длинными:

Самое короткое (3, 5) пересекается и с (0, 4), и с (4, 8): выбор его первым даёт одно мероприятие, а можно провести два.

Значит, нам важно освободиться раньше в абсолютном времени, а не просто выбрать короткое мероприятие.

Второй критерий

«Найди наибольшее количество мероприятий и наибольший перерыв между двумя последними». Как учесть второе требование?

Сначала найти наибольшее количество k, оставить первые k − 1 мероприятий жадного расписания и подобрать последнее, которое начинается как можно позже.

Жадный алгоритм выбрал (1, 3), (3, 5), (5, 8), (9, 15) — четыре мероприятия. Но последнее можно заменить на (12, 16): количество останется 4, а перерыв между двумя последними увеличится до 12 − 8 = 4. Порядок решения:

  1. Найти максимальное количество k.
  2. Взять первые k − 1 мероприятий жадного расписания.
  3. Узнать время окончания предпоследнего.
  4. Среди допустимых последних мероприятий выбрать то, которое позже начинается.

Почему первые k − 1 можно оставить жадными? Жадное построение позволяет закончить k − 1 мероприятий максимально рано. Более раннее окончание предпоследнего только увеличивает возможный перерыв, а подходящим последним становится любое мероприятие, начинающееся после него. Если гарантировано хотя бы два мероприятия, продолжение программы выше:

penultimate_finish = selected[-2][1]latest_start = Nonefor start, finish in meetings:    if start >= penultimate_finish + pause:        if latest_start is None or start > latest_start:            latest_start = startprint(len(selected), latest_start - penultimate_finish)
4 4

selected[-2] — предпоследняя выбранная запись, selected[-2][1] — время её окончания. Обрати внимание: максимизируем начало последнего мероприятия; позднее окончание само по себе не гарантирует большой перерыв. Если мероприятий меньше двух, такого перерыва нет — этот случай определяется условием.

Похожие вопросы решаются той же заменой последнего: самое позднее окончание последнего мероприятия — наибольшее finish среди мероприятий, которые начинаются не раньше penultimate_finish + pause (здесь 16).

Если в файле даны начало и длительность, окончание считают сразу при чтении: finish = start + duration. Если при одинаковом начале нужно выбрать более длительное последнее мероприятие, добавляется ещё один критерий — finish − start; на перерыв он не влияет.

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

Сервер работал в те же пять интервалов. Сколько времени он работал всего и сколько простаивал?

Объединить пересекающиеся и касающиеся интервалы в непрерывные периоды и сложить их длины; простои — промежутки между периодами.

Это другой вопрос, чем выбор непересекающихся мероприятий. Первые три образуют один непрерывный период (1, 8), последние два — (9, 16). Общая длительность работы (8 − 1) + (16 − 9) = 14.

Пять интервалов и их объединение — периоды 1–8 и 9–16. Простои — 0–1, 8–9 и 16–18.

Сначала сортируем интервалы по началу, затем храним последний уже объединённый интервал:

intervals = [(9, 15), (1, 3), (12, 16), (5, 8), (3, 5)]intervals.sort()merged = []for start, finish in intervals:    if not merged or start > merged[-1][1]:        merged.append([start, finish])    else:        merged[-1][1] = max(merged[-1][1], finish)print(*merged)
[1, 8] [9, 16]

Объединённые интервалы записаны списками [start, finish], потому что мы изменяем правую границу: merged[-1][1] = .... Почему используем max? Новый интервал может полностью лежать внутри предыдущего: после (2, 20) и (5, 8) правая граница должна остаться 20. Условие start > merged[-1][1] склеивает касающиеся интервалы: (1, 3) и (3, 5) — один период. Так и нужно, если в момент окончания работа ещё идёт или следующая начинается сразу.

Если нужен суммарный простой за период от 0 до day_end, считаем промежутки между объединёнными интервалами — продолжение программы:

day_end = 18cursor = 0idle_count = idle_duration = 0for start, finish in merged:    if start > cursor:        idle_count += 1        idle_duration += start - cursor    cursor = finishif cursor < day_end:    idle_count += 1    idle_duration += day_end - cursorprint(idle_count, idle_duration)
3 4

Учтены простой до первой работы (0–1), простои между работами (8–9) и простой после последней работы (16–18). Здесь предполагается, что все интервалы лежат внутри рассматриваемого периода.

b − a — длительность непрерывного промежутка времени от a до b, а b − a + 1 — количество целых номеров от a до b включительно. От 10:00 до 10:05 прошло 5 минут, а номера 10, 11, 12, 13, 14, 15 — это 6 номеров. Целочисленные значения времени не делают формулу длительности автоматически равной finish - start + 1.

События

Сколько процессов работало одновременно больше всего и как долго держалась такая нагрузка?

Нагрузка меняется только в моменты начала и окончания — события. Отсортировать события и идти по ним, меняя счётчик.

Проверять каждый момент времени дорого, а событий всего вдвое больше, чем интервалов. Под интервалами — сколько их работает между соседними событиями:

Между соседними событиями число работающих постоянно. Больше всего — два, с 12 до 15: нагрузка 2 держалась 3 минуты.

Для модели, в которой объект активен при start ≤ t < finish, записываем в start изменение +1, в finish — изменение -1. Эта модель называется полуинтервалом [start, finish): начало включено, окончание исключено. Изменения копим в словаре через — в один момент может быть несколько событий:

intervals = [(1, 3), (3, 5), (5, 8), (9, 15), (12, 16)]day_end = 18changes = {0: 0, day_end: 0}for start, finish in intervals:    changes[start] = changes.get(start, 0) + 1    changes[finish] = changes.get(finish, 0) - 1 times = sorted(changes)current = maximum = total_duration = 0for i in range(len(times) - 1):    current += changes[times[i]]    duration = times[i + 1] - times[i]    if current > maximum:        maximum = current        total_duration = duration    elif current == maximum:        total_duration += durationprint(maximum, total_duration)
2 3

Сортировка словаря sorted(changes) даёт отсортированный список его ключей. Между двумя соседними моментами количество активных постоянно. Почему сначала меняем current, потом учитываем длительность? Потому что после событий в момент times[i] новое количество действует до следующего события. В момент 3 одно мероприятие заканчивается, другое начинается: общее изменение −1 + 1 = 0, и количество не меняется.

Если нужен максимальный непрерывный период постоянной нагрузки, соседние участки с одинаковым количеством нужно объединять. Сам факт событий на их границе ещё не означает изменения количества — момент с нулевым итоговым изменением просто пропускают.

Окно наблюдения

А если нагрузку считают только с 8:00 до 14:00 или оба конца интервала включены?

Алгоритм тот же — меняются события: интервалы обрезают краями окна, а при включённом конце событие окончания ставят на минуту позже.

Интервалы, попавшие в окно частично, учитываются, поэтому каждый интервал обрезают краями окна: start = max(start, low), finish = min(finish, high), а если после этого start >= finish, интервал в окно не попал. Если в файле вместо начала или окончания стоит 0 со смыслом «процесс шёл с начала наблюдения» или «не закончился», его заменяют краем окна.

Если время считается целыми моментами и объект активен в оба крайних момента отрезка [start, finish], событие окончания ставят в момент finish + 1: в момент finish объект ещё работает. Сначала определяется смысл времени, затем порядок обработки событий.

Число отрезков, в течение которых количество не менялось, считают по моментам с ненулевым итоговым изменением. Для наших интервалов за период от 0 до 18:

times = [t for t in sorted(changes) if changes[t] != 0]points = [0] + times + [day_end]lengths = [b - a for a, b in zip(points, points[1:])]print(len(lengths), max(lengths), *times)
7 7 1 8 9 12 15 16

Моменты 3 и 5 выпали: в них одно мероприятие заканчивается, а другое начинается. Отрезков постоянного количества семь, самый длинный — с 1 до 8.

Итого

  • Больше всего мероприятийпо раннему окончанию
  • Второй критерийпервые k − 1 жадные, последнее — подобрать
  • Занятое времяобъединить интервалы, отсортированные по началу
  • Нагрузкасобытия +1 и −1, отсортированные по времени
  • Включённый конецсобытие окончания в finish + 1
  • Окно наблюденияобрезать интервалы краями окна

Проверьте себя

Ответь сначала сам, потом открой ответ.

Вопрос 1
Объекты активны на отрезках целых моментов [1, 3] и [3, 5], оба конца включены. Сколько объектов активны в момент 3? В какие моменты нужно записать события, чтобы программа событий это учла?
ОтветСкрыть ответ
Ответ
Два: оба отрезка включают момент 3. События: +1 в 1 и в 3, −1 в 4 и в 6.
Вопрос 2
Мероприятия (1, 4), (2, 3), (3, 6), (5, 7). Сколько можно провести без пересечений?
ОтветСкрыть ответ
Ответ
Два: например, (2, 3) и (3, 6). Жадный выбор берёт (2, 3) — оно раньше всех заканчивается, — затем (3, 6).
Вопрос 3
Мероприятия (0, 4), (4, 8), (3, 5). Почему нельзя первым брать самое короткое?
ОтветСкрыть ответ
Ответ
Самое короткое (3, 5) пересекается с обоими длинными: выбрав его, проведём одно мероприятие, а можно два — (0, 4) и (4, 8).
Вопрос 4
Сервер работал в интервалы (1, 4), (2, 6), (8, 10). Сколько он работал и сколько простаивал за период от 0 до 12?
ОтветСкрыть ответ
Ответ
Работал 7: периоды 1–6 и 8–10. Простаивал 12 − 7 = 5.
Вопрос 5
Процессы работают в полуинтервалы [0, 5), [2, 4), [3, 8). Какая наибольшая нагрузка и когда?
ОтветСкрыть ответ
Ответ
3 — с момента 3 до момента 4: в это время работают все три.

Практика

Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.

Раннее начало проигрывает

Мероприятия (0, 20), (1, 3), (3, 5), (5, 7). Сколько мероприятий можно провести и сколько выберет правило «сначала самое раннее начало»?

Введите два числа через пробел: максимальное количество, затем результат правила раннего начала.

Перерыв между последними

Мероприятия (1, 3), (2, 6), (3, 5), (5, 8), (9, 15), (12, 16). Начинать следующее можно сразу после окончания предыдущего. Найди максимальное количество и максимальный перерыв между двумя последними при таком количестве.

Введите два числа через пробел в том порядке, в каком их спрашивают.

Перерыв не меньше 1

Те же мероприятия, что в задаче 1, но между мероприятиями нужен перерыв не меньше 1. Найди максимальное количество и максимальный перерыв между двумя последними.

Введите два числа через пробел в том порядке, в каком их спрашивают.

Собрания с длительностью

Собрания заданы началом и длительностью: (1, 4), (5, 2), (6, 5), (8, 3), (12, 2). Найди максимальное количество собраний, максимальный перерыв перед последним и самое позднее окончание последнего.

Введите три числа через пробел в том порядке, в каком их спрашивают.

Зал с перерывом 10 минут

В файле 26-10.txt в первой строке записано количество заявок N, в каждой следующей — время начала и время окончания мероприятия в минутах от начала суток. Между окончанием одного мероприятия и началом следующего нужен перерыв не меньше 10 минут. Определи максимальное количество мероприятий, которые можно провести в зале, и максимально возможный при таком количестве перерыв между двумя последними мероприятиями.

26-10.txtЗаявки: начало и окончание в минутахТекстовый файл · 8,3 КБ

Введите два числа через пробел в том порядке, в каком их спрашивают.

Работа и простои сервера

Сервер работал в интервалы (2, 6), (4, 9), (10, 12), (11, 15), (20, 22). Найди общую длительность работы, количество периодов простоя за время от 0 до 24 и их суммарную длительность.

Введите три числа через пробел в том порядке, в каком их спрашивают.

Простои за сутки

В файле 26-11.txt в первой строке записано количество задач N, в каждой следующей — время начала и время окончания задачи в секундах от начала суток (0 ≤ начало < окончание ≤ 86 400). В моменты начала и окончания задача считается работающей. Определи количество периодов за сутки (от 0 до 86 400), когда не работала ни одна задача, затем их суммарную длительность в секундах.

26-11.txtЗадачи: начало и окончание в секундахТекстовый файл · 34,5 КБ

Введите два числа через пробел в том порядке, в каком их спрашивают.

Одновременные запросы

Запросы (1, 6), (2, 4), (4, 8), (5, 9) в модели полуинтервала. Найди максимальное число одновременно выполняемых запросов и общее время, когда их было столько.

Введите два числа через пробел в том порядке, в каком их спрашивают.

Наибольшая нагрузка

Для файла 26-11.txt считай, что задача работает с момента начала до момента окончания, а в одну секунду окончание одной задачи и начало другой не меняют их количество. Определи наибольшее количество одновременно работающих задач, затем общую длительность в секундах, в течение которой работало столько задач.

26-11.txtЗадачи: начало и окончание в секундахТекстовый файл · 34,5 КБ

Введите два числа через пробел в том порядке, в каком их спрашивают.