Оптимизация через разделяй-и-властвуй - Алгоритмика
Оптимизация через разделяй-и-властвуй

Оптимизация через разделяй-и-властвуй

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

Посмотрим на формулу пересчета динамики из базового решения:

f[i,j]=mink<i{f[k,j1]+(xi1xk)2} f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}

Обозначим за opt[i,j]opt[i, j] оптимальный kk для данного состояния — то есть аргминимум от выражения выше. Для однозначности, если оптимальный индекс не один, то выберем среди них самый правый.

Конкретно в задаче покрытия точек отрезками можно заметить следующее:

opt[i+1,j]opt[i,j] opt[i + 1, j] \leq opt[i, j]

Интуация такая: если нам нужно покрыть больший префикс точек, то начало последнего отрезка точно не будет раньше.

#Алгоритм

Пусть мы уже знаем opt[l,k]opt[l, k] и opt[r,k]opt[r, k] и хотим посчитать opt[i,k]opt[i, k] для какого-то ii между ll и rr. Тогда, воспользовавшись неравенством выше, мы можем сузить отрезок поиска оптимального индекса для ii со всего отрезка [0,i1][0, i - 1] до [opt[l,k],opt[r,k]][opt[l, k], opt[r, k]].

Будем делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка [l,r][l, r] на kk-том слое, зная, что их optopt лежат между ll’ и rr’. Эта функция просто берет середину отрезка [l,r][l, r] и линейным проходом считает ответ для неё, а затем рекурсивно запускается от половин, передавая в качестве границ [l,opt][l’, opt] и [opt,r][opt, r’] соответственно:

// [ l,  r] -- какие динамики на k-том слое посчитать
// [_l, _r] -- где могут быть их ответы
void solve(int l, int r, int _l, int _r, int k) {
    if (l > r)
        return; // отрезок пустой -- выходим
    int opt = _l, t = (l + r) / 2;
    // считаем ответ для f[t][k]
    for (int i = _l; i <= min(_r, t); i++) { 
        int val = f[i + 1][k - 1] + cost(i, t - 1);
        if (val < f[t][k])
            f[t][k] = val, opt = i;
    }
    solve(l,     t - 1, _l,  opt, k);
    solve(t + 1, r,     opt, _r,  k);
}

Затем последовательно вызовем эту функцию для каждого слоя:

for (int k = 1; k <= m; k++)
    solve(0, n - 1, 0, n - 1, k);

Так как отрезок [l,r][l, r] на каждом вызове уменьшается примерно в два раза, глубина рекурсии будет O(logn)O(\log n). Так как отрезки поиска для всех элементов на одном «уровне» могут пересекаться разве что только по границам, то суммарно на каждом уровне поиск проверит O(n)O(n) различных индексов. Соответственно, пересчет всего слоя займет O(nlogn)O(n \log n) операций вместо O(n2)O(n^2) в базовом решении.

Таким образом, мы улучшили асимптотику до O(nmlogn)O(n \cdot m \cdot \log n).