Изменения

Перейти к: навигация, поиск

Convex hull trick

13 335 байт добавлено, 19:21, 4 сентября 2022
м
rollbackEdits.php mass rollback
Convex hull trickПостановка примера задачи{{---}} один из методов оптимизации [[Динамическое_программирование | динамического программирования]], использующий идею [[Статические_выпуклые_оболочки: Есть n деревьев с высотами a1_Джарвис,_Грэхем,_Эндрю, a2_Чен, … an_QuickHull|выпуклой оболочки]]. Требуется спилить их всеПозволяет улучшить асимптотику решения некоторых задач, потратив минимальное количество монет на заправку бензопилы. Но пила устроена такрешемых методом динамического программирования, что после уменьшения высоты спиливаемого дерева на 1 ее надо заправить. Причем стоимость заправки зависит от срубленных с <math>O(полностьюn^2) деревьев. Если сейчас максимальный индекс срубленного дерева равен i, то цена заправки равна ci. Изначально пила заправлена.И известны следующие ограничения : c[</math> до <tex>O(n\cdot\log(n] = 0, a[1] = 1, a[i] возрастают, c[i] убывают))</tex>.Техника впервые появилась в 1995 году (Задача с codeforcesзадачу на нее предложили в USACO {{---}} национальной олимпиаде США по программированию).comМассовую известность получила после IOI (международной олимпиады по программированию для школьников)2002.
Наивное решение ==Пример задачи, решаемой методом convex hull trick==Рассмотрим задачу на ДП: Понятно{{Задача|definition = Есть <math>n</math> деревьев с высотами <tex>a_1, a_2, \dots, что нужно затратив минимальную стоимость срубить последнее a_n</tex> (n-ев метрах) дерево. Требуется спилить их все, тпотратив минимальное количество монет на заправкубензопилы.Но пила устроена так, что она может спиливать только по <math>1</math> метру от дерева, ккоторому ее применили. Также после него все деревья можно будет пилить бесплатно срубленного метра (т.к. c[n] = 0любого дерева)пилу нужно заправлять, платя за бензин определенное кол-во монет. Посчитаем следующую динамику : dp[i] - минимальная Причем стоимость, заплатив которую будет срублено дерево номер i. Тогда dp[i] = minбензина зависит от срубленных (dp[j] + a[i] * c[j]полностью) по всем j деревьев. Если сейчас максимальный индекс срубленного дерева равен < tex>i</tex>, то цена заправки равна <tex>c_i</tex>. То есть понятно Изначально пила заправлена.Также известны следующие ограничения : <tex>c_n = 0, что выгодно рубить сначала более дорогие и низкие деревьяa_1 = 1, a_i</tex> возрастают, а потом более высокие <tex>c_i</tex> убывают. Изначально пила заправлена.(убывание и дешвые возрастание нестрогие)}}(докЗадача H с Санкт-во этого факта оставляется читателям как несложное упражнение). Тогда переберем j Петербургских сборов к РОИ 2016 < i - индекс предыдущего срубленного дереваref>[http://neerc. Пусть мы его срубили отптимальным (в смысле денег) способомifmo. Тогда просто a[i] раз уменьшим высоту дерева i на 1ru/school/camp-2016/problems/20160318a. Каждый такой раз будем платить c[j] за последующую заправку пилы. Итак, на сруб ipdf Сайт с задачами Санкт-го дерева мы заплатили a[iПетербургских сборов к РОИ 2016]*c[j]. 
Нетрудно видеть, что такая динамика работает за O(n^2</ref>). </noinclude><includeonly>{{#if: {{{neat|}}}|О<div style="background-Оптимизацияcolor: #fcfcfc; float:left;"> Давайте обозначим dp[j] за b[j], а[i] за x[i], а c[j] за k[j].<div style="background-color: #ddd;">'''Задача:'''</div>Теперь формула приняла вид dp[i] <div style= min(k[j]*x[i] + b[j]) по всем j "border:1px dashed #2f6fab; padding: 8px; font-style: italic;">{{{definition}}}</div></div>|< i. Выражение k[j]*x + b[j] напоминает уравнение прямой y table border= kx + b. Сопоставим каждому j, обработанному ранее прямую y[j](x) "0" width= k[j]*x + b[j]. Из условия «c[i] убывают "100%"><tr><td style="background-color: #ddd">'''Задача:'''</td></tr> k[j] уменьшаются с номером j» следует то, что прямые, полученные ранее отсортированы в порядке убывания углового коэффицента. Давайте нарисуем несколько таких прямых <tr><td style="border:1px dashed #2f6fab; padding: 8px; background-color: #fcfcfc; font-style:italic;">{{{definition}}}</td></tr></table>}}</includeonly>
==Наивное решение==
Сначала заметим важный факт : т.к. <tex>c[i]</tex> убывают (нестрого) и <tex>c[n] = 0</tex>, то все <tex>c[i]</tex> неотрицательны.
Понятно, что нужно затратив минимальную стоимость срубить последнее (<tex>n</tex>-е) дерево, т.к. после него все деревья можно будет рубить бесплатно (т.к. <tex>c[n] = 0</tex>). Посчитаем следующую динамику : <tex>dp[i]</tex> {{---}} минимальная стоимость, заплатив которую можно добиться того, что дерево номер <tex>i</tex> будет срублено.
База динамики : <tex>dp[1] = 0</tex>, т.к. изначально пила заправлена и высота первого дерева равна <math>1</math>, по условию задачи.
Переход динамики : понятно, что выгодно рубить сначала более дорогие и низкие деревья, а потом более высокие и дешевые (док-во этого факта оставляется читателям как несложное упражнение, т.к. эта идея относится скорее к теме [[Теорема_Радо-Эдмондса_(жадный_алгоритм)|жадных алгоритмов]], чем к теме данной статьи). Поэтому перед <tex>i</tex>-м деревом мы обязательно срубили какое-то <tex>j</tex>-е, причем <tex>j \leqslant i - 1</tex>. Поэтому чтобы найти <tex>dp[i]</tex> нужно перебрать все <tex>1 \leqslant j \leqslant i - 1</tex> и попытаться использовать ответ для дерева номер <tex>j</tex>. Итак, пусть перед <tex>i</tex>-м деревом мы полностью срубили <tex>j</tex>-е, причем высота <tex>i</tex>-го дерева составляет <tex>a[i]</tex>, а т.к. последнее дерево, которое мы срубили, имеет индекс <tex>j</tex>, то стоимость каждого метра <tex>i</tex>-го дерева составит <tex>c[j]</tex>. Поэтому на сруб <tex>i</tex>-го дерева мы потратим <tex>a[i] \cdot c[j]</tex> монет. Также не стоит забывать, что ситуацию, когда <tex>j</tex>-е дерево полностью срублено, мы получили не бесплатно, а за <tex>dp[j]</tex> монет.
Итоговая формула пересчета : <tex>dp[i] = \min\limits_{j=1...i-1} (dp[j] + a[i] \cdot c[j])</tex>.
Посмотрим на код вышеописанного решения:
'''int''' <tex>\mathtt{simpleDP}</tex>('''int''' a[n], '''int''' c[n])
dp[1] = 0
dp[2] = dp[3] = ... = dp[n] = <tex>\infty</tex>
'''for''' i = 2 = 1..n
dp[i] = <tex>+\infty</tex>
'''for''' j = 1..i - 1
'''if''' (dp[j] + a[i] <tex>\cdot</tex> c[j] < dp[i])
dp[i] = dp[j] + a[i] <tex>\cdot</tex> c[j]
'''return''' dp[n]
Нетрудно видеть, что такая динамика работает за <tex>O(n^2)</tex>.
Итак==Ключевая идея оптимизации==Для начала сделаем замену обозначений. Давайте обозначим <tex>dp[j]</tex> за <tex>b[j]</tex>, давайте выделим множество точек (x0, y0) , таких что все они принадлежат одной из прямых и при этом нету ни одной прямой y’(<tex>a[i]</tex> за <tex>x)[i]</tex>, такой что y’(x0) а < y0tex>c[j]</tex> за <tex>k[j]</tex>. Иными словами возьмем «выпуклую (вверх) оболочку» нашего множества прямых. На катинке множество точек выделено жирным оранжевым цветом и представляет собой выпуклую вверх функцию. Назовем ее «y = convex(x)»
Теперь формула приняла вид <tex>dp[i] = \min\limits_{j=0...i-1}(k[j] \cdot x[i] + b[j])</tex>. Выражение <tex>k[j] \cdot x + b[j]</tex> {{---}} это в точности уравнение прямой вида <tex>y = kx + b</tex>.
Сопоставим каждому <tex>j</tex>, обработанному ранее, прямую <tex>y[j](x) = k[j] \cdot x + b[j]</tex>. Из условия «<tex>c[i]</tex> убывают <tex>\Leftrightarrow k[j]</tex> уменьшаются с номером <tex>j</tex>» следует то, что прямые, полученные ранее отсортированы в порядке убывания углового коэффициент. Давайте нарисуем несколько таких прямых :
[[Файл:picture1convexhull.png]]
Выделим множество точек <tex>(x_0, y_0)</tex> , таких что все они принадлежат одной из прямых и при этом нету ни одной прямой <tex>y’(x)</tex>, такой что <tex>y’(x_0) < y_0</tex>. Иными словами возьмем «выпуклую (вверх) оболочку» нашего множества прямых (её еще называют нижней огибающей множества прямых на плоскости). Назовем ее «<tex>y = convex(x)</tex>». Видно, что множество точек <math>(x, convex(x))</math> представляет собой выпуклую вверх функцию.
==Цель нижней огибающей множества прямых==Пусть мы считаем динамику для <tex>i</tex>-го дерева. Его задает <tex>x[i]</tex>. Итак, нам нужно для данного <tex>x[i]</tex> найти <tex>\min\limits_{j=0..i-1}(рисунок k[j] \cdot x[i] + b[j]) = \min\limits_{j=0..i-1}(y[j](x[i]))</tex>. Это выражение есть <math>convex(x[i])</math>. Из монотонности угловых коэффицентов отрезков, задающих выпуклую оболочку, и их расположения по координатам x следует то, что отрезок, который пересекает прямую <tex>x = x[i]</tex>, можно найти [[Целочисленный_двоичный_поиск|бинарным поиском]]. Это потребует <tex>O(\log(n))</tex> времени на поиск такого <tex>j</tex>, что <tex>dp[i] = k[j] \cdot x[i] + b[j]</tex>. Теперь осталось научиться поддерживать множество прямых и быстро добавлять <tex>i</tex>-ю прямую после того, как мы посчитали <tex>b[i] = dp[i]</tex>.
Воспользуемся идеей алгоритма построения выпуклой оболочки множества точек. Заведем 2 стека <tex>k[]</tex> и <tex>b[]</tex>, которые задают прямые в отсортированном порядке их угловыми коэффицентами и свободными членами. Рассмотрим ситуацию, когда мы хотим добавить новую (<tex>i</tex>-тую) прямую в множество. Пусть сейчас в множестве лежит <tex>sz</tex> прямых (нумерация с 1). Пусть <tex>(x_L, y_L)</tex> {{---}} точка пересечения <tex>sz - 1</tex>-й прямой множества и <tex>sz</tex>-й, а <tex>(x_R, y_R)</tex> {{---}} точка пересечения новой прямой, которую мы хотим добавить в конец множества и <tex>sz</tex>-й. Нас будут интересовать только их <tex>x</tex>-овые координаты <tex>x_L</tex> и <tex>x_R</tex>, соответственно. Если оказалось, что новая прямая пересекает <tex>sz</tex>-ю прямую выпуклой оболочки позже, чем <tex>sz</tex>-я <tex>sz - 1</tex>-ю, т.е. <tex>(x_L \geqslant x_R)</tex>, то <tex>sz</tex>-ю удалим из нашего множества, иначе - остановимся. Так будем делать, пока либо число прямых в стеке не станет равным 2, либо <tex>x_L</tex> не станет меньше <tex>x_R.</tex>
Для чего нам нужна эта выпуклая оболочка прямых?Асимптотика : аналогично обычному алгоритму построения выпуклой оболочки, каждая прямая ровно <math>1</math> раз добавится в стек и максимум <math>1</math> раз удалится. Значит время работы перестройки выпуклой оболочки займет <tex>O(n)</tex> суммарно.
Пусть мы считаем динамику для i-го дерева. Его задает x[i]. Итак, нам нужно для данного x[i] найти минимум по всем j k[j]*x[i] + b[i] = y[j](x[i])Файл:picture2convexhull. Нетрудно видеть, что это есть convex(x[ipng]). Из монотонности угловых коэффицентов отрезков, задающих выпуклую оболочку, и их расположения по координаты x следует то, что отрезок, который пересекает прямую x = x[i], можно найти бинарным поиском. Это потребует O(logn) времени на поиск такого j, что dp[i] = k[j] * x[i] + b[j]Файл:picture3convexhull. Теперь осталось научиться быстро поддерживать множество прямых и добавлять i-ю прямую после того, как мы посчитали b[ipng] = dp[i]. Название статьи подсказывает, что нужно воспользоваться алгоритмом построения выпуклой оболочки множества точек. Но (внезапно) у нас не точки, а прямые… Но что меняется??? Пусть есть 2 стека k[] и b[], которые задают прямые в отсортированном порядке. Пусть пришла новая прямая. Найдем точки пересечения (по x) с последними 2мя прямыми из стека. Назовем их xL и xR. Если оказалось, что новая прямая пересекает предпосл еднюю прямую выпуклой оболочки позже, чем последнюю (xL >= xR), то последнюю можно удалить из нашего множества. Так будем делать, пока либо кол-во прямых в стеке не станет равным 2 или xL не станет меньше xR. Ассимптотика : аналогично обычному алгоритму построения выпуклой оболочки, каждая прямая ровно 1 раз добавится в стек и максимум 1 раз удалится. Значит время работы перестройки выпуклой оболочки займет O(n) суммарно. Корректность : достаточно показать, что прямую нужно удалить из множества т.и т.т., когда она последнюю прямую множества наша новая прямая пересекает ее в точке с координатой по оси X, меньшей, чем предпоследнюю.
{{Теорема
|id=th1239.
|statement=Алгоритм построения нижней огибающей множества прямых корректен.
|proof=Достаточно показать, что последнюю прямую нужно удалить из множества <tex>\Leftrightarrow</tex>, когда наша новая прямая пересекает ее в точке с координатой по оси X, меньшей, чем последняя - предпоследнюю.
Пусть <tex>Y(x) = Kx + B</tex> {{---}} уравнение новой прямой, <tex>y[i](x) = K[i]x + B[i]</tex> {{---}} уравнения прямых множества. Тогда т.к. <tex>K < K[sz]</tex>, то при <tex>x \in [- \infty; x_R] : y[sz](x) <= Y(x)</tex>, а т.к. <tex> K[sz] < K[sz - 1]</tex>, то при <tex>x \in [x_L; + \infty] : y[sz - 1](x) \geqslant y[sz](x)</tex>. Если <tex>x_L < x_R</tex>, то при <tex>x \in [x_L; x_R] : y[sz - 1] \geqslant y[sz](x) и Y(x) \geqslant y[sz](x)</tex>, т.е. на отрезке <tex>[x_L; x_R]</tex> прямая номер sz лежит ниже остальных и её нужно оставить в множестве. Если же <tex>x_L > x_R</tex>, то она ниже всех на отрезке <tex>[x_L; x_R] = \varnothing </tex>, т.е. её можно удалить из множества.
}}
==Детали реализации:==
Будем хранить 2 массива : <tex>front[]</tex> {{---}} <tex>x</tex>-координаты, начиная с которых прямые совпадают с выпуклой оболочкой (т.е. i-я прямая совпадает с выпуклой оболочкой текущего множества прямых при <tex>x</tex> <tex>\in</tex> <tex>[front[i]; front[i + 1])</tex> ) и <tex>st[]</tex> {{---}} номера деревьев, соответствующих прямым (т.е. <tex>i</tex>-я прямая множества, где <tex>i</tex> <tex>\in</tex> <tex>[1; sz]</tex> соответствует дереву номер <tex>sz[i]</tex>). Также воспользуемся тем, что <tex>x[i] = a[i]</tex> возрастают (по условию задачи), а значит мы можем искать первое такое <tex>j</tex>, что <tex>x[i] \geqslant front[j]</tex> не бинарным поиском, а методом двух указателей за <tex>O(n)</tex> операций суммарно. Также массив <math>front[]</math> можно хранить в целых числах, округляя х-координаты в сторону лежащих правее по оси x до ближайшего целого (*), т.к. на самом деле мы, считая динамику, подставляем в уравнения прямых только целые <tex>x[i]</tex>, а значит если <tex>k</tex>-я прямая пересекается с <tex>k+1</tex>-й в точке <tex>z +</tex> <tex>\alpha</tex> (<math>z</math>-целое, <tex>\alpha</tex> <tex>\in</tex> <tex>[0;1)</tex>), то мы будем подставлять в их уравнения <tex>z</tex> или <tex>z + 1</tex>. Поэтому можно считать, что новая прямая начинает совпадать с выпуклой оболочкой, начиная с <tex>x = z+1</tex>
==Реализация==
'''int''' <tex>\mathtt{ConvexHullTrick}</tex>('''int''' a[n], '''int''' c[n])
st[1] = 1
front[1] = -<tex>\infty</tex><font color=green>// первая прямая покрывает все x-ы, начиная с -∞ </font>
sz = 1 <font color=green>// текущий размер выпуклой оболочки </font>
pos = 1 <font color=green>// текущая позиция первого такого j, что x[i] \geqslant front[st[j]] </font >
'''for''' i = 2..n
'''while''' (front[pos] < x[i]) <font color=green>// метод 1 указателя (ищем первое pos, такое что x[i] покрывается "областью действия" st[pos]-той прямой </font >
pos = pos + 1
j = st[pos]
dp[i] = K[j] <math>\cdot</math> a[i] + B[j]
'''if''' (i < n) <font color=green>// если у нас добавляется НЕ последняя прямая, то придется пересчитать выпуклую оболочку </font >
K[i] = c[i] <font color=green>// наши переобозначения переменных </font >
B[i] = dp[i] <font color=green>// наши переобозначения переменных </font >
x = -<tex>\infty</tex>
'''while''' ''true''
j = st[sz]
x = divide(B[j] - B[i], K[i] - K[j]) <font color=green>// x-координата пересечения с последней прямой оболочки, округленное в нужную сторону (*) </font >
'''if''' (x > from[sz]) '''break''' <font color=green>// перестаем удалять последнюю прямую из множества, если новая прямая пересекает ее позже, чем начинается ее "область действия" </font >
sz = sz - 1<font color=green>// удаляем последнюю прямую, если она лишняя </font >
st[sz + 1] = i
front[sz + 1] = x <font color=green>// добавили новую прямую </font >
sz = sz + 1
'''return''' dp[n]
Здесь функция <tex>\mathtt{divide(a, b)}</tex> возвращает нужное(*) округление <tex>\frac{a}{b}</tex>. Приведем её код :
'''int''' <tex>\mathtt{divide}</tex>('''int''' a, '''int''' b)
delta = 0
'''if''' (a '''mod''' b ≠ 0) delta = 1
'''if''' ((a > 0 '''and''' b > 0) '''or''' (a < 0 '''and''' b < 0)) '''return''' [a / b] + delta
'''return''' -[|a| / |b|]
Такая реализация будет работать за <math>O(n)</math>.
==Динамический convex hull trick==
Заметим, что условия на возрастание/убывание <tex>k[i]</tex> на убывание/возрастание и <tex>x[i]</tex> выглядят достаточно редкими для большинства задач. Пусть в задаче таких ограничений нет. Первый способ борьбы с этой проблемой - отсортировать входные данные нужным образом, не испортив свойств задачи (пример : задача G c Санкт-Петербургских сборов к РОИ 2016 <ref>[http://neerc.ifmo.ru/school/camp-2016/problems/20160318a.pdf Сайт с задачами Санкт-Петербургских сборов к РОИ 2016]</ref>).
(рис. 2) (рис. 3)   Действительно, пусть новая прямая пересекает последнюю прямую множества позже, чем предпоследнюю (рисНо рассмотрим общий случай.2 По- красная прямая новаяпрежнему у нас есть выпуклая оболочка прямых, фиолетовая - предпоследняя, желтая - последняяс помощью которой мы за <tex>O(\log(n)), то найдется такой отрезок </tex> можем найти <tex>dp[x1;x2i]</tex>, что последняяно теперь вставку <tex>i</tex>-й прямой в оболочку уже нельзя выполнить описанным ранее способом за <tex>O(желтая1) прямая при этих x-ах лежит ниже всех остальных и ее следует оставить </tex> в множествесреднем Теперь пусть новая У нас есть выпуклая оболочка, наша прямая пересекает последнюю прямую множества раньшеее, возможно, чем предпоследнюю «отсекая» несколько отрезков выпуклой оболочки в середине (рис.1), последняя 4 : красная прямая при любых x лежит выше какой-то другой прямой множества и значит ее можно удалитьта, чтд. Детали реализации: Будем хранить 2 массива (имитирующих стеки) : front[] и st[] - начало (по x) соответствующей прямой выпуклой оболочки и номер этой прямой (которую мы хотим вставить в глобальной нумерациинаше множество). Также воспользуемся тем, что Более формально : теперь наша новая прямая будет ниже остальных при <tex>x\in [ix_1; x_2] = a[i] возрастают (по условию задачи)</tex>, где <tex>x_1, а значит мы можем искать первое такое jx_2 \in R</tex> - точки пересечения с некоторыми прямыми, что xпричем <tex>x_2</tex> не обязательно равно <tex>+ \infty</tex>[i] >= front[jФайл:picture4convexhull.png] не бинарным поиском, а методом 2х указателей за O(n) суммарно. Также массив front[] можно хранить в целых числах, округляя х-координаты в сторону лежащих правее по оси x.
Чтобы уметь вставлять прямую в множество будем хранить [[Красно-черное_дерево|двоичное дерево поиска]], в вершинах которого будут пары <tex>(k, st)</tex> = (коэффицент прямой, ее номер в глобальной нумерации). Когда приходит новая прямая, ищем последнюю прямую с меньшим угловым коэффицентом, чем у той прямой, которую мы хотим добавить в множество. Поиск такой прямой занимает <tex>O(\log(n))</tex>. Начиная с найденной прямой выполняем "старый" алгоритм (удаляем, пока текущая прямая множества бесполезна). И симметричный алгоритм применяем ко всем прямым справа от нашей (удаляем правого соседа нашей прямой, пока она пересекает нас позже, чем своего правого соседа).
Асимптотика решения составит <tex>O(\log(n))</tex> на каждый из <tex>n</tex> запросов «добавить прямую» + <tex>O(n\cdot\log(n))</tex> суммарно на удаление прямых, т.к. по-прежнему каждая прямая не более одного раза удалится из множества, а каждое удаление из std::set занимает <tex>O(\log(n))</tex> времени. Итого <math>O(n\cdot\log(n))</math>.
== Альтернативный подход ==
Другой способ интерпретировать выражение <tex>dp[i] = \min\limits_{j=0...i-1}(c[j] \cdot a[i] + dp[j])</tex> заключается в том, что мы будем пытаться свести задачу к стандартной выпуклой оболочке множества точек. Перепишем выражение средующим образом : <tex>dp[j] + a[i] \cdot c[j] = (dp[j], c[j]) \cdot (1, a[i])</tex>, т.е. запишем как скалярное произведение векторов <tex>v[j] = (dp[j], c[j])</tex> и <tex >u[i] = (1, a[i])</tex >. Вектора <tex >v[j] = (dp[j], c[j])</tex> хотелось бы организовать так, чтобы за <tex >O(\log(n))</tex> находить вектор, максимизирующий выражение <tex>v[j] \cdot u[i]</tex>. Посмотрим на рис. 5. Заметим интуитивно очевидный факт : красная точка (вектор) <tex>j</tex> не может давать более оптимальное значение <tex>v[j] \cdot u[i]</tex> одновременно чем обе синие точки. По этой причине нам достаточно оставить выпуклую оболочку векторов <tex>v[j]</tex>, а ответ на запрос {{---}} это поиск <tex>v[j]</tex>, максимизирующего проекцию на <tex>u[i]</tex>. Это задача поиска ближайшей точки выпуклого многоугольника (составленного из точек выпуклой оболочки) к заданной прямой (из <tex>(0, 0)</tex> в <tex>(1, a[i])</tex>). Ее можно решить за <tex>O(\log(n))</tex> двумя бинарными или одним тернарным поиском
Асимптотика алгоритма по-прежнему составит <tex>O(n \cdot \log(n))</tex>
Р[[Файл:picture5convexhull.Реализацияpng]]
st[0] = 0from[0] = -∞sz = 1 Докажем то, что описанный выше алгоритм корректен. Для этого достаточно показать, что если имеются <math>3</math> вектора <math>a, b, c</math>, расположенные как на рис. 5, т.е. точка <math>b</ текущий размер math> не лежит на выпуклой оболочкиpos = оболочке векторов <tex>0 , a, b, c </tex> : <tex> \Leftrightarrow [a-b, b-c] < 0 </ текущая позиция первго такого jtex>, что xто либо <tex>(a, u[i] )</tex> оптимальнее, чем <tex>= front(b, u[st[ji]]for )</tex>, i = 1..n-1 while либо <tex>(frontc, u[posi] )</tex> оптимальнее, чем < xtex>(b, u[i]) ++pos</tex>.{{Теорема j |id= st[pos]th12392. dp[i] |statement= KЕсли есть <tex>3</tex> вектора <tex>a, b, c</tex>, таких что <tex>[j] * a[i-b, b-c] + B[j] if < 0</tex> то либо <math>(i a, u) < n - 1(b, u) </math>, либо <math>(c, u) < (b, u)</ если у нас добавляется НЕ последняя прямаяmath>, K[i] где вектор <math>u = b[i] B[i] = dp[i] ll x = -inf while (1; k) </math>. j |proof= stПо условию теоремы известно, что <tex>[sz a-b, b- 1c] < 0 \Leftrightarrow (a_{x} - b_{x = divide})\cdot(B[j] b_{y} - B[i], K[i] c_{y}) < (a_{y} - K[j]b_{y}) if \cdot (b_{x > from[sz } - 1]c_{x}) break --sz st[sz] = i from[sz++] = x </tex> (Здесь функция divide*). Предположим (aот противного), что <tex>(b, u) возвращает нужное округление < (a , u) \Leftrightarrow b_{x} + k \cdot b_{y} < a_{x} + k \cdot a_{y} \Leftrightarrow (b_{x} - a_{x}) < k \cdot (a_{y} - b_{y})</ tex> и при этом <tex>(b, u) < (c, u)\Leftrightarrow b_{x} + k \cdot b_{y} < c_{x} + k \cdot c_{y} \Leftrightarrow (c_{x} - b_{x}) > k \cdot (b_{y} - c_{y})</tex>.
Такая реализация будет работать за OПодставим эти неравенства в (n*).Получим цепочку неравенств : <tex>k \cdot (a_{y} - b_{y})</tex><tex> \cdot (c_{y} - b_{y}) = k</tex><tex> \cdot (b_{y} - a_{y}) \cdot </tex><tex>(b_{y} - c_{y})</tex> <tex> < (a_{x} - b_{x})</tex><tex> \cdot (b_{y} - c_{y})</tex><tex> < (a_{y} - b_{y}) \cdot </tex><tex>(b_{x} - c_{x})</tex> <tex>< k \cdot (a_{y} - b_{y})</tex><tex> \cdot (c_{y} - b_{y})</tex>. Получили противоречие : <tex>k \cdot (a_{y} - b_{y}) \cdot (c_{y} - b_{y}) < k \cdot (a_{y} - b_{y}) \cdot (c_{y} - b_{y})</tex>. Значит предположение неверно, чтд.}}
Динамический convex hull trickИз доказанной теоремы и следует корректность алгоритма.
Заметим, что условия на прямые, что k[i] возрастает/убывает и x[i] убывает/возрастает выглядят не достаточно общими. Как же быть, если в задаче таких ограничений нет. Иногда можно отсортировать прямые нужным образом, не испортив свойств задачи (пример : задача G отсюда http://neerc.ifmo.ru/school/camp-2016/problems/20160318a.pdf). Но рассмотрим общий случай. Наша задача поменялась следующим образом : по-прежнему у нас есть выпуклая оболочка, имея которую мы за O(logn) или быстрее можем найти dp[i], но теперь вставку i-й прямой в оболочку уже нельзя выполнить старым способом за O(1) (в среднем). У нас есть выпуклая оболочка, наша прямая пересекает ее, возможно, «отрезая» несколько отрезков выпуклой оболочки в середине (рис. 4)==См. также==
Т.е. нужно уметь быстро (за O(logn)?) назодить, после какой прямой стоит пытаться вставить текущую (красную рис.4) примую и удалять лишние справа, начиная с нее, потом проводить аналогичные операции слева. Итак, давайте хранить std*[[:Статические_выпуклые_оболочки:set (или любой аналог в других языках) пар <k, st> = <коэффицент прямой, ее номер в глобальной нумерации>. Когда приходит новая прямая_Джарвис, делаем lower_bound - 1 в сете_Грэхем, т.е. ищем ближайшую прямую с меньшим углом наклона_Эндрю, и начиная с нее повторяем старый алгоритм (удаляем_Чен, пока прямая бесполезная). И симметричный алгоритм применяем ко всем прямым справа от нашей. Ассимптотика решения составит O(logn) на каждый из n запросов «добавить прямую» + O(n) суммарно на удаление прямых. Итго O(nlogn)._QuickHull|Выпуклая оболочка]]
Альтернативный подход Другой способ интерпретировать выражение dp[i] = max(dp[j] + a[i] * c[j]) по всем их заключается в следующем: давайте перепишем выражение dp[j] + a[i] * c[j] = (dp[j], c[j]) * (1, a[i]), т.е. запишем ка скалярное произведение векторов v[j] = (dp[j], c[j]) и u[i] = (1, a[i]). Вектора v[j] = (dp[j], c[j]) хотелось бы организовать так, чтобы за O(logn) находить максимизирующий выражение v[j] * u[i]. Посмотрим на рис. 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) двумя бинарными или одним тернарным поиском
== Примечания ==
<references/>
Ассимптотика алгоритма по-прежнему составит O(nlogn)[[Категория:Дискретная математика и алгоритмы]][[Категория: Динамическое программирование]][[Категория: Способы оптимизации методов динамического программирования]]
1632
правки

Навигация