# теория · шаг 2 из 7
Разбор: где поставить склад на трассе
Коротко
Вторая задача из практики: магазины стоят вдоль трассы, нужно выбрать километр для склада. Здесь всё решает вопрос «что значит лучше». Если минимизировать сумму пробега с учётом числа заказов - ответ взвешенная медиана: магазин, на котором при обходе слева впервые набирается половина заказов. Если минимизировать путь до самого дальнего магазина - середина между крайними. На одних и тех же шести магазинах это 55-й и 29-й километр, а «медиана без учёта заказов» даёт 10-й и 1580 км пробега в день против 916. Два склада сводятся к разрезу отсортированного списка: 5 вариантов вместо 1378.
Если ты знаешь, почему сумма расстояний минимальна в медиане, а наибольшее - в середине, листай до «Теперь сам».
Как думаешь, где ставить
Шесть магазинов вдоль трассы, на километрах 3, 8, 10, 11, 40 и 55. Где поставить склад, чтобы машины ездили меньше? Ответь себе до чтения.
Первое, что хочется сказать, - «где-то посередине» или «поближе к кучке магазинов в начале трассы». А теперь добавим: магазин на 55-м километре - гипермаркет, у него 30 заказов в день, у остальных вместе 22. Ответ меняется полностью - и это сигнал, что в просьбе «чтобы ездили меньше» спрятано несколько разных задач.
На что это похоже
Перетягивание каната. Сдвинь склад на километр вправо - все магазины справа станут ближе, все слева - дальше. Если справа «тянут» сильнее (больше заказов), сдвигаться выгодно. Склад останавливается там, где силы уравнялись, - а это точка, по каждую сторону от которой не больше половины заказов.
Механизм: от просьбы к модели
Вход - километры магазинов и число заказов в день; склад ставится на целом километре, у отметки трассы. Вид ответа - лучший: одна точка. Что значит «лучше» - три честных толкования:
- сумма пробега: каждый заказ - отдельная поездка, минимизируем
Σ |x − pᵢ| · wᵢ; - сумма без весов: каждый магазин - одна поездка в день, независимо от заказов;
- самый дальний: важна гарантия «доставка не дольше часа», минимизируем
max |x − pᵢ|. Час переводится в километры, потому что машины идут по одной трассе с одной скоростью: время доставки пропорционально расстоянию. Например, при 60 км/ч час - это 60 км, и склад, до дальнего магазина от которого 26 км, гарантию выполняет. Если скорость на участках разная, мерить надо минутами, и это уже другая модель.
Размер - сотни магазинов, но точку можно искать не перебором, а рассуждением.
Почему медиана. Пусть склад в x; L - суммарный вес магазинов левее x и в самой точке x, R -
строго правее. Сдвиг на километр вправо (пока по дороге нет магазинов) уменьшает пробег правых на R и
увеличивает пробег остальных на L, то есть меняет сумму на L − R. Пока L < R, сдвигаться вправо
выгодно; как только L ≥ R, уже нет. Значит, минимум - в магазине, на котором накопленный слева вес вместе
с ним самим впервые достигает половины. Это взвешенная медиана. На примере ниже: левее 55-го километра
22 заказа из 52, а вместе с гипермаркетом - 52, половина набирается ровно на нём. Для наибольшего расстояния
работает другое рассуждение: дальний магазин всегда один из крайних, и уравнять расстояние до обоих можно
только в середине между ними.
Правило
Когда «лучше» означает сумму расстояний, ответ - (взвешенная) медиана; когда наибольшее расстояние - середина между крайними. Прежде чем считать, выясни, какое из толкований нужно: на одних и тех же данных они дают разные точки.
Разбор: три критерия - три склада
Перебор по всем целым километрам - исполняемая спецификация, медиана и середина - быстрые ответы; сверяем. Перебирать трассу целиком не нужно: склад за крайним магазином хуже, чем на самом крайнем, - сдвиг внутрь приближает его ко всем сразу. Поэтому перебор идёт от левого крайнего магазина до правого, какими бы ни были их километры.
# магазины вдоль трассы: [километр, заказов в день]
magaziny = [[3, 10], [8, 4], [10, 1], [11, 2], [40, 5], [55, 30]]
def summa(x):
# километров в день: каждый заказ едет со склада x к магазину
# (только в одну сторону; туда-обратно - всё вдвое, ответ тот же)
s = 0
for m in magaziny:
s = s + abs(m[0] - x) * m[1]
return s
def hudshee(x):
# самый дальний магазин
h = 0
for m in magaziny:
h = max(h, abs(m[0] - x))
return h
def perebor(cel):
# от крайнего левого магазина до крайнего правого
levyj = min(magaziny)[0]
pravyj = max(magaziny)[0]
luchshij = levyj
for x in range(levyj, pravyj + 1):
if cel(x) < cel(luchshij):
luchshij = x
return luchshij
def vzveshennaya_mediana():
# точка, где набралась половина всех заказов при обходе слева
vsego = 0
for m in magaziny:
vsego = vsego + m[1]
nakopleno = 0
for m in sorted(magaziny):
nakopleno = nakopleno + m[1]
if 2 * nakopleno >= vsego:
return m[0]
def bez_vesov():
# медиана положений, если заказы не учитывать
km = []
for m in magaziny:
km.append(m[0])
km.sort()
return km[(len(km) - 1) // 2]
x = perebor(summa)
print("сумма с заказами: перебор", x, "медиана", vzveshennaya_mediana(),
"| км в день", summa(x))
x = bez_vesov()
print("медиана без заказов:", x, "| км в день с заказами", summa(x))
x = perebor(hudshee)
# склад - на целом километре: если середина крайних между отметками,
# обе соседние отметки одинаково хороши, и // берёт левую
print("самый дальний: перебор", x, "середина крайних", (3 + 55) // 2)
print(" до дальнего", hudshee(x), "| км в день", summa(x))
сумма с заказами: перебор 55 медиана 55 | км в день 916
медиана без заказов: 10 | км в день с заказами 1580
самый дальний: перебор 29 середина крайних 29
до дальнего 26 | км в день 1234
Гипермаркет на 55-м километре один держит больше половины заказов, поэтому взвешенная медиана - прямо у него, и пробег 916 км в день. Медиана без весов ставит склад на 10-й километр, в кучку мелких магазинов, - и машины проезжают 1580 км: на 72% больше. Критерий «самый дальний» даёт 29-й километр, гарантию «не дальше 26 км» - и 1234 км пробега. Ни один ответ не ошибка: каждый - оптимум своей задачи. Ошибка - выбрать критерий молча.
Разбор: два склада - разрез отсортированного списка
Бюджет позволяет два склада. Перебор пар километров от 3-го до 55-го - 53 · 52 / 2 = 1378 вариантов, а
при трассе в 1000 км - полмиллиона. Модель сокращает работу: каждый магазин ездит на ближайший склад,
поэтому магазины, отсортированные по километру, делятся на левую часть (к левому складу) и правую. Почему
непрерывными частями: если склады стоят в a < b, магазин левее середины между ними ближе к a, правее -
к b. Середина одна, значит, все магазины левого склада лежат левее всех магазинов правого. Остаётся
перебрать место разреза, а в каждой части склад - её взвешенная медиана по первому разбору.
magaziny = [[3, 10], [8, 4], [10, 1], [11, 2], [40, 5], [55, 30]]
def stoimost(sklady):
# каждый магазин обслуживает ближайший склад
s = 0
for m in magaziny:
blizhe = 10 ** 9
for x in sklady:
blizhe = min(blizhe, abs(m[0] - x))
s = s + blizhe * m[1]
return s
# перебор: все пары километров от крайнего магазина до крайнего
proverok = 0
luchshee = [10 ** 9, 0, 0]
for a in range(3, 56):
for b in range(a + 1, 56):
proverok = proverok + 1
c = stoimost([a, b])
if c < luchshee[0]:
luchshee = [c, a, b]
print("перебор пар: ", luchshee, "вариантов", proverok)
def mediana(gruppa):
vsego = 0
for m in gruppa:
vsego = vsego + m[1]
nakopleno = 0
for m in gruppa:
nakopleno = nakopleno + m[1]
if 2 * nakopleno >= vsego:
return m[0]
# модель: ближайший склад делит отсортированные магазины на левую и
# правую часть; в каждой части склад - её взвешенная медиана
po_poryadku = sorted(magaziny)
luchshee = [10 ** 9, 0, 0]
for i in range(1, len(po_poryadku)):
a = mediana(po_poryadku[:i])
b = mediana(po_poryadku[i:])
c = stoimost([a, b])
if c < luchshee[0]:
luchshee = [c, a, b]
print("разрез списка: ", luchshee, "вариантов", len(po_poryadku) - 1)
перебор пар: [118, 3, 55] вариантов 1378
разрез списка: [118, 3, 55] вариантов 5
Почему перебор разрезов не может промахнуться, хотя stoimost потом переназначает магазины ближайшему
складу. Возьмём лучшую пару складов; её магазины делятся каким-то разрезом. На этом разрезе медиана
каждой части обслуживает свою часть не хуже, чем склад из лучшей пары, - медиана оптимальна для части
по первому разбору. А переназначение ближайшему складу стоимость только уменьшает. Значит, на этом
разрезе программа получит не больше оптимума, а меньше оптимума не бывает.
Ответ совпал: склады на 3-м и 55-м километрах, 118 км пробега в день. Вариантов - 5 разрезов вместо 1378
пар. Граница правила: так просто только на прямой. Для k складов разрез превращается в динамику по
отрезкам отсортированного списка (раздел 15), а на плоскости или в дорожной сети задача о k складах с
минимальной суммой - k-медиана - NP-трудна, и там уже нужны приближения и локальный поиск (раздел 22).
Что ломается без этого
Критерий выбрали молча. Отчёт обещает «оптимальное размещение», логисты меряют пробег, а алгоритм минимизировал самое дальнее расстояние. Разница здесь - 35%.
Веса забыли. Медиана без учёта заказов ставит склад в кучку мелких магазинов, далеко от того, кто даёт больше половины поездок.
Перебор там, где хватает модели. Перебор пар километров на трассе в 1000 км - полмиллиона вариантов, разрез отсортированного списка - сотни.
Зачем это в работе
Тот же выбор критерия стоит за любой «оптимизацией расположения»: где держать реплику базы (средняя задержка или худшая?), какую версию считать «типичной» в отчёте (среднее или медиана?), куда ставить точку выдачи. Медиана устойчива к далёким точкам: один магазин на краю карты или один сбойный замер сдвигают её на одну позицию, а середину крайних - на половину расстояния. К весу она не равнодушна, и так и надо: куда больше поездок, туда и тянет. А «самый дальний» нужен там, где есть гарантия перед каждым, а не в среднем.
Теперь сам
- Магазины на километрах 2, 5, 9, 20, по одному заказу в день у каждого. Где склад по сумме пробега? Где по самому дальнему?
- К тем же магазинам добавили пятый, на 100-м километре, тоже с одним заказом. Как сдвинутся оба ответа?
- Почему при чётном числе магазинов без весов годится любая точка между двумя средними?
Ответ. 1. По сумме - любая точка от 5 до 9 (две средние), пробег 22 км. По дальнему - середина (2 +
20) / 2 = 11, до дальнего 9 км.
-
Медиана - ровно 9-й километр (третий из пяти): из отрезка от 5 до 9 осталась одна точка, его правый край. Середина крайних -
(2 + 100) / 2 = 51: уехала на 40 км. Медиана устойчива к далёкой точке, середина крайних - нет. -
Между двумя средними слева и справа поровну магазинов,
L = R, и сдвиг не меняет сумму.
Главное
«Где поставить склад» - три разные задачи, и ответы у них разные: взвешенная медиана для суммы пробега, середина крайних для худшего случая. Выбор критерия - часть спецификации, а модель «ближайший склад делит список» превращает перебор пар в перебор разрезов.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий