Convex hull trick — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Для чего нам нужна эта выпуклая оболочка прямых?)
(Для чего нам нужна эта выпуклая оболочка прямых?)
Строка 22: Строка 22:
 
[[Файл:picture2convexhull.png]]  
 
[[Файл:picture2convexhull.png]]  
 
[[Файл:picture3convexhull.png]]
 
[[Файл:picture3convexhull.png]]
Действительно, пусть новая прямая пересекает последнюю прямую множества позже, чем предпоследнюю (рис.2 - красная прямая новая, фиолетовая - предпоследняя, желтая - последняя), то найдется такой отрезок <math>[x1;x2]</math>, что последняя(желтая) прямая при этих x-ах лежит ниже всех остальных и ее следует оставить в множестве.
+
 
Теперь пусть новая прямая пересекает последнюю прямую множества раньше, чем предпоследнюю (рис.1),  последняя прямая при любых x лежит выше какой-то другой прямой множества и значит ее можно удалить, чтд.
+
Действительно, пусть новая прямая пересекает последнюю прямую множества позже, чем предпоследнюю (рис.2 - красная прямая новая, фиолетовая - предпоследняя, желтая - последняя), то найдется такой отрезок <math>[x1;x2]</math>, что последняя(желтая) прямая при этих x-ах лежит ниже всех остальных и ее следует оставить в множестве.
 +
Теперь пусть новая прямая пересекает последнюю прямую множества раньше, чем предпоследнюю (рис.1),  последняя прямая при любых x лежит выше какой-то другой прямой множества и значит ее можно удалить, чтд.
  
 
==Детали реализации:==
 
==Детали реализации:==

Версия 20:53, 23 ноября 2016

Постановка примера задачи

Есть n деревьев с высотами [math]a1, a2, … an[/math]. Требуется спилить их все, потратив минимальное количество монет на заправку бензопилы. Но пила устроена так, что после уменьшения высоты спиливаемого дерева на 1 ее надо заправить. Причем стоимость заправки зависит от срубленных (полностью) деревьев. Если сейчас максимальный индекс срубленного дерева равен i, то цена заправки равна ci. Изначально пила заправлена. И известны следующие ограничения : [math]c[n] = 0, a[1] = 1, a[i][/math] возрастают, [math]c[i][/math] убывают. (Задача с H отсюда : http://neerc.ifmo.ru/school/camp-2016/problems/20160318a.pdf)

Наивное решение

Понятно, что нужно затратив минимальную стоимость срубить последнее ([math]n[/math]-е) дерево, т.к. после него все деревья можно будет пилить бесплатно (т.к. [math]c[n] = 0[/math]). Посчитаем следующую динамику : [math]dp[i][/math] - минимальная стоимость, заплатив которую будет срублено дерево номер [math]i.[/math] Тогда [math]dp[i] = min_{j=0...i-1}(dp[j] + a[i] * c[j])[/math]. То есть понятно, что выгодно рубить сначала более дорогие и низкие деревья, а потом более высокие и дешвые (док-во этого факта оставляется читателям как несложное упражнение). Тогда переберем [math]j \lt i[/math] - индекс предыдущего срубленного дерева. Пусть мы его срубили отптимальным (в смысле денег) способом. Тогда просто [math]a[i][/math] раз уменьшим высоту дерева i на 1. Каждый такой раз будем платить [math]c[j][/math] за последующую заправку пилы. Итак, на сруб [math]i[/math]-го дерева мы заплатили [math]a[i]*c[j][/math]. 
Нетрудно видеть, что такая динамика работает за [math]O(n^2)[/math]

О-Оптимизация

Давайте обозначим [math]dp[j][/math] за [math]b[j][/math], [math]а[i][/math] за [math]x[i][/math], а [math]c[j][/math] за [math]k[j][/math]. Теперь формула приняла вид [math]dp[i] = min_{j=0...i-1}(k[j]*x[i] + b[j])[/math]. Выражение [math]k[j]*x + b[j][/math] напоминает уравнение прямой [math]y = kx + b[/math]. Сопоставим каждому [math]j[/math], обработанному ранее прямую [math]y[j](x) = k[j]*x + b[j][/math]. Из условия «[math]c[i][/math] убывают [math]\lt =\gt k[j][/math] уменьшаются с номером [math]j[/math]» следует то, что прямые, полученные ранее отсортированы в порядке убывания углового коэффицента. Давайте нарисуем несколько таких прямых :

Picture1convexhull.png

Итак, давайте выделим множество точек [math](x0, y0)[/math] , таких что все они принадлежат одной из прямых и при этом нету ни одной прямой [math]y’(x)[/math], такой что [math]y’(x0) \lt y0[/math]. Иными словами возьмем «выпуклую (вверх) оболочку» нашего множества прямых. На катинке множество точек выделено жирным оранжевым цветом и представляет собой выпуклую вверх функцию. Назовем ее «[math]y = convex(x)[/math]»

Для чего нам нужна эта выпуклая оболочка прямых?

Пусть мы считаем динамику для [math]i[/math]-го дерева. Его задает [math]x[i][/math]. Итак, нам нужно для данного [math]x[i][/math] найти минимум по всем [math]min_{j=0..i-1}(k[j]*x[i] + b[i]) = min_{j=0..i-1}(y[j](x[i]))[/math]. Нетрудно видеть, что это есть convex(x[i]). Из монотонности угловых коэффицентов отрезков, задающих выпуклую оболочку, и их расположения по координаты x следует то, что отрезок, который пересекает прямую [math]x = x[i][/math], можно найти бинарным поиском. Это потребует O(logn) времени на поиск такого j, что dp[i] = k[j] * x[i] + b[j]. Теперь осталось научиться быстро поддерживать множество прямых и добавлять [math]i[/math]-ю прямую после того, как мы посчитали [math]b[i] = dp[i][/math]. Название статьи подсказывает, что нужно воспользоваться алгоритмом построения выпуклой оболочки множества точек. Но (внезапно) у нас не точки, а прямые… Но что меняется??? Пусть есть 2 стека [math]k[] и b[][/math], которые задают прямые в отсортированном порядке. Пусть пришла новая прямая. Найдем точки пересечения (по x) с последними 2мя прямыми из стека. Назовем их [math]xL[/math] и [math]xR[/math]. Если оказалось, что новая прямая пересекает предпосл еднюю прямую выпуклой оболочки позже, чем последнюю (xL >= xR), то последнюю можно удалить из нашего множества. Так будем делать, пока либо кол-во прямых в стеке не станет равным 2 или [math]xL[/math] не станет меньше [math]xR.[/math] Ассимптотика : аналогично обычному алгоритму построения выпуклой оболочки, каждая прямая ровно 1 раз добавится в стек и максимум 1 раз удалится. Значит время работы перестройки выпуклой оболочки займет [math]O(n)[/math] суммарно. Корректность : достаточно показать, что прямую нужно удалить из множества т.и т.т., когда она последнюю прямую множества наша новая прямая пересекает ее в точке с координатой по оси X, меньшей, чем предпоследнюю. Picture2convexhull.png Picture3convexhull.png

Действительно, пусть новая прямая пересекает последнюю прямую множества позже, чем предпоследнюю (рис.2 - красная прямая новая, фиолетовая - предпоследняя, желтая - последняя), то найдется такой отрезок [math][x1;x2][/math], что последняя(желтая) прямая при этих x-ах лежит ниже всех остальных и ее следует оставить в множестве. Теперь пусть новая прямая пересекает последнюю прямую множества раньше, чем предпоследнюю (рис.1), последняя прямая при любых x лежит выше какой-то другой прямой множества и значит ее можно удалить, чтд.

Детали реализации:

Будем хранить 2 массива (имитирующих стеки) : [math]front[][/math] и [math]st[][/math] - начало (по x) соответствующей прямой выпуклой оболочки и номер этой прямой (в глобальной нумерации). Также воспользуемся тем, что [math]x[i] = a[i][/math] возрастают (по условию задачи), а значит мы можем искать первое такое [math]j[/math], что [math]x[i] \gt = front[j][/math] не бинарным поиском, а методом 2х указателей за [math]O(n)[/math] суммарно. Также массив front[] можно хранить в целых числах, округляя х-координаты в сторону лежащих правее по оси x.

Р.Реализация

            st[0] = 0
            from[0] = -∞
            sz = 1 // текущий размер выпуклой оболочки
            pos = 0 // текущая позиция первго такого j, что x[i] >= front[st[j]]
            for  i = 1..n-1  {
                   	while (front[pos] < x[i]) 
                   			++pos
                   	j = st[pos]
                    	dp[i] = K[j] * a[i] + B[j]
                    	if (i < n - 1)  // если у нас добавляется НЕ последняя прямая, 
                       		K[i] = b[i]
                       		B[i] = dp[i]
                        	        x = -inf
                        	        while (1) {
            		                      j = st[sz - 1]
              		                      x = divide(B[j] - B[i], K[i] - K[j])
            	 	                      if (x > from[sz - 1]) break
              	              	              --sz
                                       }
                       st[sz] = i
                     from[sz++] = x
            }          

(Здесь функция divide(a, b) возвращает нужное округление a / b) Такая реализация будет работать за O(n).

Динамический convex hull trick

Заметим, что условия на прямые, что [math]k[i][/math] возрастает/убывает и [math]x[i][/math] убывает/возрастает выглядят не достаточно общими. Как же быть, если в задаче таких ограничений нет. Иногда можно отсортировать прямые нужным образом, не испортив свойств задачи (пример : задача G отсюда http://neerc.ifmo.ru/school/camp-2016/problems/20160318a.pdf). Но рассмотрим общий случай. Наша задача поменялась следующим образом : по-прежнему у нас есть выпуклая оболочка, имея которую мы за [math]O(logn)[/math] или быстрее можем найти [math]dp[i][/math], но теперь вставку i-й прямой в оболочку уже нельзя выполнить старым способом за [math]O(1)[/math] (в среднем). У нас есть выпуклая оболочка, наша прямая пересекает ее, возможно, «отрезая» несколько отрезков выпуклой оболочки в середине (рис. 4). Т.е. нужно уметь быстро (за [math]O(logn)[/math]?) назодить, после какой прямой стоит пытаться вставить текущую (красную рис.4) примую и удалять лишние справа, начиная с нее, потом проводить аналогичные операции слева. Итак, давайте хранить [math]std::set[/math] (или любой аналог в других языках) пар [math]\lt k, st\gt [/math] = [math]\lt коэффицент прямой, ее номер в глобальной нумерации\gt [/math]. Когда приходит новая прямая, делаем lower_bound - 1 в сете, т.е. ищем ближайшую прямую с меньшим углом наклона, и начиная с нее повторяем старый алгоритм (удаляем, пока прямая бесполезная). И симметричный алгоритм применяем ко всем прямым справа от нашей. Ассимптотика решения составит [math]O(logn)[/math] на каждый из n запросов «добавить прямую» + [math]O(n)[/math] суммарно на удаление прямых. Итго [math]O(nlogn)[/math].

Альтернативный подход

Другой способ интерпретировать выражение [math]dp[i] = max(dp[j] + a[i] * c[j])[/math] по всем их заключается в следующем: давайте перепишем выражение [math]dp[j] + a[i] * c[j] = (dp[j], c[j]) * (1, a[i])[/math], т.е. запишем ка скалярное произведение векторов [math]v[j] = (dp[j], c[j]) и u[i] = (1, a[i])[/math]. Вектора [math]v[j] = (dp[j], c[j])[/math] хотелось бы организовать так, чтобы за O(logn) находить максимизирующий выражение [math]v[j] * u[i][/math]. Посмотрим на рис. 5. Заметим довольно очевидный факт : красная точка(вектор) j не может давать более оптимальное значение v[j] * u[i] одновременно чем обе синие точки, т.к. v[j] * u[i] - это на самом деле проекция вектора v[j] на u[i]. По этой причине нам достаточно оставить выпуклую оболочку векторов v[j], а ответ на запрос - это поиск v[j], максимизирующего проекцию на u[i]. Это задача поиска ближайшей точки выпуклого многоугольника (составленного из точек выпуклой оболочки) к заданной прямой (из (0, 0) в (1, a[i])). Ее можно решить за O(logn) двумя бинарными или одним тернарным поиском Ассимптотика алгоритма по-прежнему составит [math]O(n*log(n))[/math]