Convex Hull Trick - Алгоритмика
Convex Hull Trick

Convex Hull Trick

Эта статья — одна из серии. Рекомендуется сначала прочитать все предыдущие.

Возьмём исходную формулу для ff и раскроем скобки в cost:

f[i,j]=mink<i{f[k,j1]+(xi1xk)2}=mink<i{f[k,j1]+xi122xi1xk+xk2} \begin{aligned} f[i, j] &= \min_{k < i} \{ f[k, j-1] + (x_{i-1}-x_k)^2 \} \\ &= \min_{k < i} \{ f[k, j-1] + x_{i-1}^2 - 2x_{i-1} x_k + x_k^2 \} \end{aligned} Заметим, что xi12x_{i-1}^2 не зависит от kk, значит его можно вынести. Под минимумом тогда останется только (f[k,j1]+xk2)ak+(2xk)bkxi1 \underbrace{(f[k, j-1] + x_k^2)}_{a_k} + \underbrace{(-2 x_k)}_{b_k} \cdot x_{i-1} Выполнив перегруппировку, получаем, что исходное выражение можно переписать как f[i,j]=mink{(ak,bk)(1,xi1)} f[i, j] = \min_k \{ (a_k, b_k) \cdot (1, x_{i-1}) \}

где под «\cdot» имеется в виду скалярное произведение.

#Алгоритм

Пусть мы хотим найти оптимальное kk для f[i,j]f[i, j]. Представим все уже посчитанные релевантные динамики с предыдущего слоя как точки (ak,bk)(a_k, b_k) на плоскости.

Чтобы эффективно находить среди них точку с минимальным скалярным произведением, можно поддерживать их нижнюю огибающую — вектор (1,xi1)(1, x_{i-1}) «смотрит» всегда вверх, поэтому нам интересна только она — и бинпоиском по ней находить оптимальную точку.

Хранить нижнюю огибающую можно просто в стеке. Так как добавляемые точки отсортированы по xx, её построение будет занимать линейное время, а асимптотика всего алгоритма будет упираться в асимптотику бинарного поиска, то есть будет равна O(nmlogn)O(n m \log n)

struct line {
    int k, b;
    line() {}
    line(int a, int _b) { k = a, b = _b; }
    int get(int x) { return k * x + b; }
};

vector<line> lines; // храним прямые нижней огибающей
vector<int> dots; // храним x-координаты точек нижней огибающей
//     ^ первое правило вещественных чисел
//      считаем, что в dots лежит округленная вниз x-координата

int cross(line a, line b) { // считаем точку пересечения
                            // считаем a.k > b.k
    int x = (b.b - a.b) / (a.k - b.k);
    if (b.b < a.b) x--; // боремся с округлением у отрицательных чисел
    return x;
}


void add(line cur) {
    while (lines.size() && lines.back().get(dots.back()) > cur.get(dots.back())) {
        lines.pop_back();
        dots.pop_back();
    }
    if (lines.empty())
        dots.push_back(-inf);
    else 
        dots.push_back(cross(lines.back(), cur));
    lines.push_back(cur);
}

int get(int x) {
    int pos = lower_bound(dots.begin(), dots.end(), x) - dots.begin() - 1;
    return lines[pos].get(x);
}

В случае нашей конкретной задачи, алгоритм можно и дальше соптимизировать, если вспомнить, что opt[i,j]opt[i][j+1]opt[i, j] \leq opt[i][j+1], то есть что оптимальная точка всегда будет «правее». Это позволяет вместо бинпоиска применить метод двух указателей, и таким образом соптимизировать решение до O(nm)O(n m).