이 글은 시리즈의 일부입니다. 먼저 이전 글을 모두 읽기를 권장합니다.
$f$에 대한 원래 공식을 가져와 cost의 괄호를 풀어 봅시다:
여기서 «$\cdot$»는 내적을 의미합니다.
#알고리즘
$f[i, j]$에 대한 최적의 $k$를 찾고자 한다고 합시다. 이전 층에서 이미 계산된 관련 DP 값들을 모두 평면 위의 점 $(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)$까지 최적화할 수 있습니다.