# теория · шаг 2 из 7
Сначала крупные: сортировка перед раздачей и граница 4/3
Коротко
В раздаче задач по серверам ошибку жадности целиком создаёт последняя задача: она садится поверх уже
выровненной нагрузки. Если отсортировать задачи по убыванию длины и только потом раздавать (правило
LPT - longest processing time, «самые длинные вперёд»), последней в очереди окажется самая короткая, и
задача, которая закончится позже всех, уже не может быть длинной относительно оптимума. Простой довод даёт гарантию 3/2, аккуратный - 4/3 − 1/(3m), и на специально построенном
входе она достигается. На случайных входах средняя ошибка измеряется долями процента.
Если ты докажешь гарантию 3/2 для раздачи после сортировки и знаешь, откуда в точной оценке берётся 4/3, листай до «Теперь сам».
Как думаешь, что изменит одна сортировка
В прошлом уроке жадная раздача на худшем входе ошибалась почти вдвое: m(m − 1) единичных задач
ложились ровно, а последняя длинная садилась сверху. Отсортируем задачи по убыванию до раздачи и больше
ничего не тронем. Всё ещё жадность, всё ещё одна строка кода.
Ответь себе до чтения: какой станет гарантия? Останется двойка, потому что жадность есть жадность? Или
одна сортировка доведёт до оптимума? Ни то ни другое. Гарантия падает с 2 − 1/m до 4/3 − 1/(3m), а
оптимума сортировка не даёт: есть вход из пяти задач на двух серверах, где она ошибается на одну шестую.
На что это похоже
Укладка багажника. Опытный водитель сначала ставит чемоданы, потом сумки, а мелочь распихивает по щелям в самом конце. Кто кладёт в порядке «как вынесли из квартиры», рискует, что последним вынесут самый большой чемодан - и он не влезет никуда, кроме как сверху.
Мелочь в конце безопасна: даже если положить её неудачно, перекос будет на размер этой мелочи. Ровно это и превращается в доказательство: перекос жадности не больше длины последней задачи, а последней после сортировки идёт короткая.
Механизм: почему последняя задача теперь короткая
Из прошлого урока: если позже всех закончилась задача ℓ, то длина расписания
T ≤ P / m + p_ℓ, где P - вся работа. Первое слагаемое не больше оптимума всегда. Вся игра - во
втором: насколько длинной может быть задача, которая закончилась последней.
Довод на 3/2. Задачи пронумерованы по убыванию: p₁ ≥ p₂ ≥ … ≥ pₙ.
случай 1: ℓ <= m
задача ℓ попала на пустой сервер и стоит на нём одна:
T = p_ℓ <= OPT
случай 2: ℓ > m
задачи 1, 2, ..., m + 1 все не короче p_ℓ;
m + 1 задач на m серверах - две из них в оптимуме на одном сервере,
значит OPT >= 2 · p_ℓ, то есть p_ℓ <= OPT / 2
T <= P / m + p_ℓ <= OPT + OPT / 2 = 1,5 · OPT
Довод опирается на принцип ящиков: m + 1 предметов в m ящиках - хотя бы в одном ящике два. Новая
нижняя граница 2 · p_{m+1} появилась только благодаря сортировке: без неё задача ℓ могла быть самой
длинной.
Откуда 4/3. Если последняя задача не длиннее OPT / 3, та же оценка даёт T ≤ OPT + OPT / 3.
Остаётся случай, когда все задачи до ℓ длиннее трети оптимума. Тогда в оптимальном расписании на
сервере их не больше двух, и разбор случаев (здесь он не приводится - его можно найти у Грэма или в
книге Уильямсона и Шмойса) показывает, что для таких входов раздача после сортировки оптимальна -
раскладывать приходится только пары. Точная оценка Грэма (1969): 4/3 − 1/(3m).
Правило
Раздача по списку после сортировки по убыванию (LPT) даёт длину не больше (4/3 − 1/(3m)) · OPT.
Простой довод на 3/2: последней кончается задача не длиннее OPT / 2, потому что первые m + 1
задач не короче её и две из них в оптимуме делят сервер. Ошибка жадности не больше длины последней
задачи - сортировка делает её короткой.
Разбор: тот же вход и семейство худших входов
razdat - раздача по списку из прошлого урока, lpt - она же после сортировки. Сначала семь задач из
прошлого урока, потом семейство входов, на котором гарантия LPT достигается: на m серверах по две
задачи длины 2m − 1, 2m − 2, …, m + 1 и ещё три задачи длины m.
def razdat(zadachi, m):
nagruzka = [0] * m
for p in zadachi:
luchshij = 0
for i in range(1, m):
if nagruzka[i] < nagruzka[luchshij]:
luchshij = i
nagruzka[luchshij] = nagruzka[luchshij] + p
return max(nagruzka)
def lpt(zadachi, m):
return razdat(sorted(zadachi, reverse=True), m)
print(razdat([3, 7, 5, 3, 1, 9, 1], 3), lpt([3, 7, 5, 3, 1, 9, 1], 3))
for m in [2, 3, 4, 5]:
zadachi = []
for d in range(2 * m - 1, m, -1):
zadachi.append(d)
zadachi.append(d)
zadachi = zadachi + [m, m, m]
print(m, zadachi, lpt(zadachi, m), 3 * m, 4 / 3 - 1 / (3 * m))
15 10
2 [3, 3, 2, 2, 2] 7 6 1.1666666666666665
3 [5, 5, 4, 4, 3, 3, 3] 11 9 1.222222222222222
4 [7, 7, 6, 6, 5, 5, 4, 4, 4] 15 12 1.25
5 [9, 9, 8, 8, 7, 7, 6, 6, 5, 5, 5] 19 15 1.2666666666666666
На входе из прошлого урока сортировка дала 10 вместо 15 - ровно оптимум. Девятка встала первой, и мелочь легла вокруг неё.
Семейство - честный худший случай. На двух серверах: 3, 3, 2, 2, 2. Сортировка кладёт тройки на
разные серверы, двойки по одной к каждой - нагрузки 5, 5, - и последняя двойка садится сверху: 7.
Оптимум 6: 3 + 3 и 2 + 2 + 2. Отношение 7 / 6 = 4/3 − 1/6. В общем случае ответ 4m − 1 против
оптимума 3m. Оптимум собирается парами: задача длины d встаёт вместе с задачей 3m − d - обе есть
во входе, - и каждая такая пара занимает сервер ровно на 3m; три задачи длины m занимают последний
сервер на те же 3m. Столбец гарантии совпал с отношением ответ / оптимум на каждой строке:
11 / 9 = 1,222, 15 / 12 = 1,25, 19 / 15 = 1,267.
Разбор: гарантия против типичной ошибки
Гарантия описывает худший вход. Сколько алгоритм теряет на обычных? Сорок случайных наборов по десять задач длиной от 1 до 20, два сервера. Оптимум для двух серверов считается точно таблицей достижимых сумм из раздела о динамическом программировании: лучшее, что может взять первый сервер, - наибольшая достижимая сумма, не превышающая половины.
import random
def razdat(zadachi, m):
nagruzka = [0] * m
for p in zadachi:
luchshij = 0
for i in range(1, m):
if nagruzka[i] < nagruzka[luchshij]:
luchshij = i
nagruzka[luchshij] = nagruzka[luchshij] + p
return max(nagruzka)
def optimum_dvuh(zadachi):
vsego = sum(zadachi)
dostizhimo = [False] * (vsego + 1)
dostizhimo[0] = True
for p in zadachi:
for s in range(vsego, p - 1, -1):
if dostizhimo[s - p]:
dostizhimo[s] = True
s = vsego // 2
while not dostizhimo[s]:
s = s - 1
return vsego - s
random.seed(22)
hudshee_z = 0
hudshee_l = 0
summa_z = 0
summa_l = 0
for opyt in range(40):
zadachi = []
for i in range(10):
zadachi.append(random.randint(1, 20))
opt = optimum_dvuh(zadachi)
z = razdat(zadachi, 2) / opt
l = razdat(sorted(zadachi, reverse=True), 2) / opt
summa_z = summa_z + z
summa_l = summa_l + l
hudshee_z = max(hudshee_z, z)
hudshee_l = max(hudshee_l, l)
print(round(summa_z / 40, 3), round(hudshee_z, 3))
print(round(summa_l / 40, 3), round(hudshee_l, 3))
1.062 1.2
1.009 1.039
Первая строка - раздача в порядке прихода: в среднем 6,2% сверх оптимума, худший из сорока - 20%.
Вторая - после сортировки: 0,9% в среднем, 3,9% в худшем. Гарантии на двух серверах - 1,5 и
7/6 ≈ 1,167; на случайных входах оба алгоритма далеко от них.
Два вывода. Сортировка улучшила и гарантию, и типичное поведение - так бывает не всегда, но здесь да. И типичная ошибка на порядок меньше гарантии: гарантия нужна не для того, чтобы предсказать ошибку на обычном входе, а чтобы её не боялись на необычном.
Что ломается без этого
Задачи приходят по одной. Сортировка требует знать все задачи заранее. Если задачи поступают
потоком и решать надо сразу, LPT недоступна, и остаётся раздача по приходу с её 2 − 1/m. Сравнивать
алгоритмы, не спросив, знаешь ли ты вход целиком, - ошибка постановки, а не выбора.
Сортируют не по тому. По возрастанию - худший выбор: самые длинные задачи приходят последними и садятся на выровненную нагрузку, ровно как во втором разборе прошлого урока.
Ждут оптимума. LPT не точна: на 3, 3, 2, 2, 2 она даёт 7 при оптимуме 6. Точный ответ - задача о
разбиении, NP-трудная. Если нужна точность выше 4/3, нужен другой инструмент - схема приближенияРазбирается в разделе 22, глава «Пределы приближения - PTAS, APX и почему дальше нельзя»: PTAS и FPTAS: точность по заказу и кто её не допускает
с точностью по заказу.
Зачем это в работе
Ночной пересчёт отчётов: две сотни заданий разной длины, двенадцать воркеров, всё известно заранее.
Раздать в порядке очереди - получить ночь, которая иногда кончается к обеду. Отсортировать по оценке
длительности - потерять в худшем случае треть, а обычно доли процента. Одна строка sorted - самая
дешёвая оптимизация в планировщике.
Тот же приём в упаковке: архивы по дискам, контейнеры по узлам кластера, заказы по курьерам. Правило «сначала крупное» - не фольклор, а то, что убирает из оценки самый опасный член.
Теперь сам
- Задачи
5, 5, 4, 4, 3, 3, 3, три сервера. Раздай по правилу LPT и найди оптимум. Совпало ли отношение с гарантией4/3 − 1/9? - Почему в доводе на
3/2нужны именноm + 1задач не корочеp_ℓ, а неm? - На четырёх серверах LPT дала 26. Какое наименьшее значение может иметь оптимум, если верить точной гарантии?
Ответ. 1. Первые три задачи 5, 5, 4 встают по одной на пустые серверы. Вторая четвёрка идёт
на самый свободный - третий: нагрузки 5, 5, 8. Две тройки - на первый и второй: 8, 8, 8. Последняя
тройка садится сверху: 11. Оптимум 9: 5 + 4, 5 + 4, 3 + 3 + 3. 11 / 9 = 4/3 − 1/9
- совпало.
-
mзадач могут стоять наmразных серверах по одной, и никакой новой границы не выйдет. Нужна лишняя задача, чтобы по принципу ящиков две оказались вместе. -
Гарантия при
m = 4:4/3 − 1/12 = 15/12 = 1,25.OPT ≥ 26 / 1,25 = 20,8, а длины целые - значит,OPT ≥ 21.
Главное
Ошибка жадной раздачи сидит в последней задаче. Сортировка по убыванию делает последнюю задачу короткой, а лишняя задача сверх числа серверов даёт новую нижнюю границу: так одна строка кода меняет гарантию с двойки на четыре трети.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий