Эта статья — одна из серии. Рекомендуется сначала прочитать все предыдущие.
Возьмём исходную формулу для $f$ и раскроем скобки в cost
:
где под «$\cdot$» имеется в виду скалярное произведение.
#Алгоритм
Пусть мы хотим найти оптимальное $k$ для $f[i, j]$. Представим все уже посчитанные релевантные динамики с предыдущего слоя как точки $(a_k, b_k)$ на плоскости.
Чтобы эффективно находить среди них точку с минимальным скалярным произведением, можно поддерживать их нижнюю огибающую — вектор $(1, x_{i-1})$ «смотрит» всегда вверх, поэтому нам интересна только она — и бинпоиском по ней находить оптимальную точку.
Хранить нижнюю огибающую можно просто в стеке. Так как добавляемые точки отсортированы по $x$, её построение будет занимать линейное время, а асимптотика всего алгоритма будет упираться в асимптотику бинарного поиска, то есть будет равна $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] \leq opt[i][j+1]$, то есть что оптимальная точка всегда будет «правее». Это позволяет вместо бинпоиска применить метод двух указателей, и таким образом соптимизировать решение до $O(n m)$.