Автор задачі: | Ігор Мельникович (Melnykovych_Ihor) |
Підготував: | Ігор Мельникович (Melnykovych_Ihor) |
Аналіз: | Андрій Холод (Connect) |
Розв’язання
Ключова ідея: максимум піраміди
Позначимо mx = \max(a). У будь-якій піраміді максимум — це деяке значення і всі числа 1..H-1 мають з’явитися по обидва боки від максимуму.
Оскільки ми можемо лише вставляти 1 та збільшувати, то зменшувати елементи не можна. Тому природно будувати піраміду висотою не меншою за поточний максимум, і в оптимальному розв’язку (для цієї реалізації) розглядається основний випадок H = mx.
Нехай всі позиції з максимальним значенням у початковому масиві утворюють відрізок [l, r] (де — перша поява , — остання).
1) Розбиття на ліву та праву частину
Ми будуємо дві "півпіраміди", які з’єднуються в максимумі:
Ліва частина: елементи a_0..a_l (включно з першим максимумом).
Права частина: елементи a_{n-1}..a_r у зворотному порядку (тобто дивимось справа наліво, включно з останнім максимумом).
У коді це масиви та .
Далі є допоміжна функція: [ solve(b, mx) \rightarrow (ans, res) ] яка для даного "краю" рахує дві величини:
— повна вартість, щоб зробити цю частину сумісною з пірамідою висоти без використання "безкоштовної вставки " як трюку;
— те саме, але з урахуванням максимально можливого "знижкового" ефекту (місце, куди вигідно вставити безкоштовний так, щоб зекономити 1 операцію).
2) Що робить solve(a, mx)
Нехай вхідний масив — це частина, яка має стати неспадною до піка (бо ми будуємо "підйом" до максимуму).
Спочатку робимо неспадним: [ a_i \leftarrow \max(a_i, a_{i-1}) ] Вартість збільшень додається в .
Далі йдемо справа наліво і "прибиваємо" значення так, щоб справа можна було сформувати спад mx, mx-1, \dots:
Для кожного елемента беремо [ new\_val = \max(a_i, mx) ] і платимо new\_val - a_i (бо тільки збільшення дозволені). Якщо new\_val = mx, то ми "використали" це значення для піраміди і зменшуємо потрібну висоту: [ mx \leftarrow mx - 1 ] Таким чином алгоритм намагається по можливості заповнити рівні mx, mx-1, \dots, 1 реальними елементами (з мінімальними підняттями).
Якщо після проходу залишилось mx > 0, то числа доведеться добрати вставками одиниць та підняттями (фактично це коштує суму): [ 1 + 2 + \dots + mx = \frac{mx(mx+1)}{2} ] Цей доданок додається до .
"Знижка" (discount) — це максимальна кількість послідовних елементів справа, які ми піднімали (тобто де new\_val > a_i). Інтуїція така: якщо в цю ділянку вставити безкоштовний елемент , можна зекономити 1 операцію (бо один з підйомів/вставок можна замінити безкоштовною вставкою).
Також враховується випадок, коли ще залишився дефіцит рівнів — тоді вставка може "покрити" один з потрібних рівнів.
Повертається: [ res = ans - discount ] тобто найкращий варіант з "однією безкоштовною вставкою" для цієї половини.
3) Як поєднати ліво і право
Є два сценарії: максимум піраміди утворюється зліва чи справа (бо вставку можна використати в різних місцях). Тому для фінальної відповіді ми комбінуємо: [ \min(resL.ans + resR.res,\; resL.res + resR.ans) ] тобто в одній половині використовуємо "покращений" варіант (із знижкою), а в іншій — звичайний.
4) Випадок, коли максимум зустрічається кілька разів
Якщо l \ne r, то у нас в початковому масиві є відрізок максимумів [l,r].
По-перше, всі елементи всередині цього відрізка, які менші за , треба підняти до (бо максимум має бути однаковим значенням на "плато" до обробки): [ bonus = \sum_{i=l}^{r} (mx - a_i) ] Це просто обов’язкова доплата.
По-друге, піраміда вимагає єдиний максимум. Тому нам треба "розірвати" плато. Це робиться використанням вставки як нового піка або як елемента, який дозволяє зробити пік у потрібному місці. У коді розглядаються два підвипадки:
Якщо r-l+1 > 2, то достатньо 1 додаткової операції (вставка 1 або підняття) для коректного формування єдиного піка, тому пробується: [ \min(resL.ans + resR.res,\; resL.res + resR.ans) + 1 ] разом з базовим resL.ans + resR.ans.
Якщо плато дуже коротке (), то треба акуратно "перевірити" обидва варіанти, прибираючи з однієї сторони (тимчасово), перераховуючи solve, і додаючи .
Фінальна відповідь друкується як: [ ans + bonus ]
5) Окремі крайні випадки
Якщо mx=1:
при n=1 треба вставити ще один елемент, щоб максимум був унікальний відповідь ;
інакше масив вже можна зробити пірамідальним безкоштовною вставкою (вставляємо у потрібне місце), тому відповідь .
Асимптотика
Функція solve працює за для довжини масиву . В основній програмі вона викликається константну кількість разів (до кількох перерахунків у випадку короткого плато), тому загальна асимптотика: Пам’ять:
//#pragma GCC optimize("Ofast", "unroll-loops")
//#pragma GCC target("sse", "sse2", "sse3", "ssse3", "sse4")
#include <bits/stdc++.h>
#define all(a) a.begin(),a.end()
#define len(a) (int)(a.size())
#define mp make_pair
#define pb push_back
#define fi first
#define se second
using namespace std;
typedef pair<int, int> pii;
typedef long long ll;
typedef long double ld;
template<class T>
using vec = vector<T>;
template<typename T>
bool umin(T &a, T b) {
if (b < a) {
a = b;
return true;
}
return false;
}
template<typename T>
bool umax(T &a, T b) {
if (a < b) {
a = b;
return true;
}
return false;
}
#ifdef KoRoVa
#define DEBUG for (bool _FLAG = true; _FLAG; _FLAG = false)
#define LOG(...) print(#__VA_ARGS__" ::", __VA_ARGS__) << endl
template <class ...Ts> auto &print(Ts ...ts) { return ((cerr << ts << " "), ...); }
#else
#define DEBUG while (false)
#define LOG(...)
#endif
const int max_n = -1, inf = 1000111222;
pair<ll, ll> solve (vector <int> a, int mx) { // [1, mx]
int n = len(a);
ll ans = 0;
for (int i = 1; i < n; i++) {
if (a[i] < a[i - 1]) {
ans += a[i - 1] - a[i];
a[i] = a[i - 1];
}
}
vector <int> b;
int cnt = 0, discount = 0;
for (int i = n - 1; i >= 0; i--) {
int new_val = max(a[i], mx);
ans += new_val - a[i];
if (new_val == mx) {
--mx;
}
if (new_val > a[i]) {
++cnt;
}
else {
cnt = 0;
}
umax(discount, cnt);
}
assert(mx >= 0);
ans += mx * (mx + 1ll) / 2;
umax(discount, mx + cnt);
ll res = ans - discount;
LOG(ans, res, discount);
return make_pair(ans, res);
}
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
vector <int> a(n);
for (auto &i : a) cin >> i;
int mx = *max_element(all(a));
if (mx == 1) {
if (n == 1) {
cout << "1\n";
return 0;
}
cout << "0\n";
return 0;
}
int l = -1, r = -1;
for (int i = 0; i < n; i++) {
if (a[i] == mx) {
r = i;
if (l == -1) {
l = i;
}
}
}
vector <int> b[2];
for (int i = 0; i <= l; i++) {
b[0].pb(a[i]);
}
for (int i = n - 1; i >= r; i--) {
b[1].pb(a[i]);
}
auto resL = solve(b[0], mx);
auto resR = solve(b[1], mx);
if (l != r) {
ll bonus = 0;
for (int i = l; i <= r; i++) {
bonus += mx - a[i];
}
ll ans = resL.first + resR.first;
if (r - l + 1 > 2) {
umin(ans, min(resL.first + resR.second, resL.second + resR.first) + 1);
}
else {
for (int it = 0; it < 2; it++) {
assert(b[it].back() == mx);
b[it].pop_back();
auto res1 = solve(b[0], mx);
auto res2 = solve(b[1], mx);
b[it].pb(mx);
umin(ans, min(res1.first + res2.second, res1.second + res2.first) + 1);
}
}
cout << ans + bonus << '\n';
return 0;
}
cout << min(resL.first + resR.second, resL.second + resR.first) << '\n';
}
/*
KoRoVa!
*/