Перейти до основного вмісту

ДП «розділяй і володарюй»

«Розділяй і володарюй» — це оптимізація динамічного програмування.

Коли підходить цей алгоритм?
  • Рекурентне співвідношення має вигляд dp(i,j)=minkdp(i1,k1)+C(k,j)dp(i, j) = \min_{k} \\{ dp(i - 1, k - 1) + C(k, j) \\}, а прямолінійне ДП дає O(mn2)O(m n^2), яке потрібно прискорити?
  • Чи виконується умова монотонності opt(i,j)opt(i,j+1)opt(i, j) \leq opt(i, j + 1) (зокрема, якщо функція вартості CC задовольняє нерівність чотирикутника C(a,c)+C(b,d)C(a,d)+C(b,c)C(a, c) + C(b, d) \leq C(a, d) + C(b, c) для abcda \leq b \leq c \leq d)? (якщо ні → монотонність не гарантована, оптимізація незастосовна)
  • Якщо вартість лінійна за параметром розбиття — чи не зручніше застосувати трюк з опуклою оболонкою?

Передумови

Деякі задачі динамічного програмування мають рекурентне співвідношення такого вигляду:

dp(i,j)=min0kjdp(i1,k1)+C(k,j)dp(i, j) = \min_{0 \leq k \leq j} \\{ dp(i - 1, k - 1) + C(k, j) \\}

де C(k,j)C(k, j)функція вартості, а dp(i,j)=0dp(i, j) = 0 при j<0j \lt 0.

Нехай 0i<m0 \leq i \lt m і 0j<n0 \leq j \lt n, а обчислення CC займає O(1)O(1) часу. Тоді прямолінійне обчислення наведеного рекурентного співвідношення дає O(mn2)O(m n^2). Маємо m×nm \times n станів і nn переходів для кожного стану.

Нехай opt(i,j)opt(i, j) — це значення kk, яке мінімізує наведений вираз. Припускаючи, що функція вартості задовольняє нерівність чотирикутника, ми можемо показати, що opt(i,j)opt(i,j+1)opt(i, j) \leq opt(i, j + 1) для всіх i,ji, j. Це відоме як умова монотонності. Тоді ми можемо застосувати ДП «розділяй і володарюй». Точка оптимального розбиття для фіксованого ii зростає зі зростанням jj.

Це дозволяє нам обчислити всі стани ефективніше. Нехай ми обчислили opt(i,j)opt(i, j) для деяких фіксованих ii та jj. Тоді для будь-якого j<jj' < j ми знаємо, що opt(i,j)opt(i,j)opt(i, j') \leq opt(i, j). Це означає, що під час обчислення opt(i,j)opt(i, j') нам не доведеться розглядати так багато точок розбиття!

Щоб мінімізувати час роботи, ми застосуємо ідею «розділяй і володарюй». Спершу обчислимо opt(i,n/2)opt(i, n / 2). Потім обчислимо opt(i,n/4)opt(i, n / 4), знаючи, що воно менше або дорівнює opt(i,n/2)opt(i, n / 2), та opt(i,3n/4)opt(i, 3 n / 4), знаючи, що воно більше або дорівнює opt(i,n/2)opt(i, n / 2). Рекурсивно відстежуючи нижню та верхню межі для optopt, ми досягаємо часу роботи O(mnlogn)O(m n \log n). Деталі реалізації дивіться у коді нижче.

Щоб довести складність «розділяй і володарюй», спершу зауважимо, що в рекурсії є O(logn)O(\log{n}) рівнів. Ми стверджуємо, що на кожному рівні виконується O(n)O(n) кроків. Нехай сумарна довжина інтервалів opt\text{opt} (позначених optloptl та optroptr у коді) на kk-му рівні дорівнює SkS_k, і зауважимо, що щоразу, коли інтервал з рівня kk довжини xx розбивається, отримані інтервали мають сумарну довжину щонайбільше x+1x + 1. Крім того, на рівні kk виконується щонайбільше 2k2^k розбиттів, тож маємо Sk+1Sk+2kS_{k + 1} \leq S_k + 2^k. Застосовуючи цю оцінку індуктивно з S0=nS_0 = n, отримуємо, що для кожного рівня kk,

Sk<n+2kO(n).S_k < n + 2^k \in O(n).

Таким чином, складність кожного «розділяй і володарюй» — O(nlogn)O(n\log{n}), а складність усього обчислення ДП — O(mnlogn)O(mn\log{n}).

Загальна реалізація

Хоч реалізація і змінюється залежно від задачі, ось доволі загальний шаблон. Функція compute обчислює один рядок ii станів dp_cur, маючи попередній рядок i1i-1 станів dp_before. Її слід викликати як compute(0, n-1, 0, n-1). Функція solve обчислює m рядків і повертає результат.

int m, n;
vector<long long> dp_before, dp_cur;

long long C(int i, int j);

// обчислюємо dp_cur[l], ... dp_cur[r] (включно)
void compute(int l, int r, int optl, int optr) {
if (l > r)
return;

int mid = (l + r) >> 1;
pair<long long, int> best = {LLONG_MAX, -1};

for (int k = optl; k <= min(mid, optr); k++) {
best = min(best, {(k ? dp_before[k - 1] : 0) + C(k, mid), k});
}

dp_cur[mid] = best.first;
int opt = best.second;

compute(l, mid - 1, optl, opt);
compute(mid + 1, r, opt, optr);
}

long long solve() {
dp_before.assign(n,0);
dp_cur.assign(n,0);

for (int i = 0; i < n; i++)
dp_before[i] = C(0, i);

for (int i = 1; i < m; i++) {
compute(0, n - 1, 0, n - 1);
dp_before = dp_cur;
}

return dp_before[n - 1];
}

На що звернути увагу

Найбільша складність у задачах на ДП «розділяй і володарюй» — це довести монотонність optopt. Один окремий випадок, коли вона виконується, — це коли функція вартості задовольняє нерівність чотирикутника, тобто C(a,c)+C(b,d)C(a,d)+C(b,c)C(a, c) + C(b, d) \leq C(a, d) + C(b, c) для всіх abcda \leq b \leq c \leq d. Багато задач на ДП «розділяй і володарюй» можна також розв'язати за допомогою трюку з опуклою оболонкою (Convex Hull trick) і навпаки. Корисно знати й розуміти обидва підходи!

Задачі для практики

References