logo search
Высокоцровневые методы информатики и првые методы информатики и программированияограммирования

5.8.1 Задача о выборе заявок

Пусть подано N заявок на прове- дение занятий в одной и той же ау- дитории. Для каждого занятия из- вестно время его проведения [bi,ei], где bi – время начала заня- тия, еi – время окончания. Два раз- ных занятия не могут перекры- ваться по времени, но условимся, что начало одного может совпа- дать с окончанием другого. Зада- ча состоит в том, чтобы выбрать максимальное число совместимых по времени занятий.

Жадный алгоритм поступает так: упорядочим заявки в порядке воз- растания времени окончания: .На сортировку понадобится време- ни O( log(N)) в худшем случае. Так как в этой статье проблема сортировки не рассматривается, положим, что массив уже отсорти- рован по возрастанию. Далее все предельно просто:

var

b,е: array[1..100] of integer; {входные данные} А: set of byte; {искомое множество}

i,j,n: integer;

begin

read(n);

for i:=1 to n do read(b[i],e[i]);

А := [1]; {начальное значение} j := 1; {самый правый отрезок множества А – первый}

for i:=2 to n do

if b[i]>=е[j] then begin

(если i-й отрезок лежит npaвee j-го) А:=А+[i];

(то включаем j-й )

j := i; (и запоминаем его как самый правый в множестве А)

end;

for i:=1 to n do (выводим результат)

if (i in А) then writeln(i);

end.

Листинг 5.21 – Задача о выборе заявок, рещаемая «жадным» алгоритмом

Вначале А содержит заяв- ку j=1. Далее в цикле по i ищется заявка, начинающаяся не рань- ше, чем оканчивается заявка j. Если таковая найдена, она вклю- чается в множество А, и перемен- ной j присваивается ее номер. Ал- горитм требует всего лишь O(n) шагов (без учета предваритель- ной сортировки). Жадность этого алгоритма состоит в том, что на каждом шаге он делает выбор так, чтобы остающееся свобод- ным время было максимальным (это и есть локально-оптималь- ный выбор).

Теперь докажем, что этот алго- ритм дает оптимальное решение. Прежде всего, докажем, что суще- ствует оптимальное решение за- дачи о выборке заявок, содержа- щее заявку 1 (с самым ранним временем окончания). В самом деле, если в каком-то оптималь- ном множестве заявок заявка 1 не содержится, то можно заменить в нем заявку с самым ранним вре- менем окончания на заявку 1, что не повредит совместности заявок, (ибо заявка 1 кончается еще раньше, чем прежняя, и ни с чем пере- сечься не может) и не изменит их общего количества. Стало быть, можно искать оптимальное мно- жество заявок А среди содержащих заявку 1. После того, как мы договорились рассматривать только наборы, содержащие заяв- ку 1, все несовместные с ней за- явки можно выкинуть, и задача сводится к выбору оптимального набора заявок из множества ос- тавшихся заявок (совместных с заявкой 1). Другими словами, мы свели задачу к аналогичной зада- че с меньшим числом заявок. Pac суждая по индукции, получаем, что, делая на каждом шаге жад- ный выбор, мы придем к опти- мальному решению.

Вспомним динамическое про- граммирование и попробуем ре- шить ту же задачу с помощью не- го. Заведем динамическую табли- цу m[1..n], где m[i] означает мак- симальное число совместных за- явок среди набора с номерами 1..i. Допустим, что подзадачи m[1], m[2],...,m[k1] уже реше- ны. Установим рекуррентную за- висимость для решения задачи m[k]: k-й отрезок можно брать, а можно и не брать в искомое мно- жество. Если мы его не берем, то m[k] = m[k1], а если мы его бе- рем, то

m[k] = 1 + m[ max( i такое, что e[i] ≤ s[k]) ]

Последнее выражение означает, что мы отбросили все заявки, не- совместные с k‑й (левее нее), взя- ли оптимум для оставшегося мно- жества из динамической таблицы плюс заявку k. Таким образом, рекуррентная зависимость такова

m[k] = max{ m[k1], 1 + m[max(i та- кое, что e[i] ≤ b[k]) ])

Для того, чтобы найти оптимальное множество (а не только количество m[n] элементов в нем), надо завес- ти дополнительный массив рrеv[1..n], где prev[k] будет озна- чать предыдущий элемент, (если мы k-й брали), или 1 (если не бра- ли k-й). Покажем, как выглядит ре- ализация ДП-алгоритма, а потом сделаем его оценку.

var

b,е,m,prev: array[0..100] of integer;

i,j,k,n: integer;

b egin

read(n); for i:=1 to n do read(b[i],e[i]);

fillchar(prev, sizeof(prev), $FF);(заполняем -1)

m [0] := 0;

for k:=2 to n do begin

i:= k-1; (ищем i такое, что e[i]=b[k])

while (i>0) and (е[i] >b[k] ) do dec(i);

if m[k-1] >= 1 + m[i] then

(если элемент лучше не брать)

m[k] := m[k-1] (то не берем его)

else begin

m[k]:= 1 + m[i];

(иначе берем и)

prev[k] := i; (запоминаем, что перед ним идет элемент i)

end;

end;

i:=n; (пробежимся с конца до начала и выведем все элементы)

repeat

if рrеv[i] =-1 then dec(i)

(если i-Й не брали, движемся дальше)

еlsе begin (в противном случае)

writeln(i); (выводим этот и)

i:= prev[i);

(перепрыгиваем через несовместимые слева от него)

end;

until i=0;

end.

Листинг 5.22 – Задача о выборе заявок, рещаемая ДП-алгоритмом

Даже не делая оценок сложности, легко видеть, что ДП-алгоритм сложнее жадного. Ну, а если быть более точным, то его сложность равняется O(n2), так как существу- ет два вложенных цикла (for по k и внутренний while, который делает в с реднем порядка O(n) сравнений и уменьшений i). К тому же он требу- ется порядка O(n) дополнительной памяти. Таким образом, в этом слу- чае жадный алгоритм намного эф- фективнее динамического прог- раммирования.