Автор задачі: | Андрій Холод (Connect) |
Підготував: | Андрій Холод (Connect) |
Розбір: | Андрій Холод (Connect) |
Ідея
Ми робимо preorder-обхід дерева: вершина записується в масив у момент входу в неї, а порядок обходу синів можна вибирати довільно.
Потрібно максимізувати кількість позицій, у яких значення є новим рекордом, тобто строго більшим за все, що вже зустрічалось раніше.
Іншими словами, під час обходу ми підтримуємо поточний максимум. Якщо в момент входу в вершину її значення більше за нього, то отримуємо до відповіді.
Уся складність задачі в тому, що піддерева можна переставляти місцями.
1) Що важливо про піддерево
Для кожної вершини заздалегідь порахуємо:
cnt[v] — розмір піддерева;
mx[v] — максимальне значення в піддереві.
Це робиться простим DFS.
Почему mx[v] важливий? Бо якщо поточний рекорд уже не менший за mx[v], то з усього піддерева ми більше не отримаємо жодного нового префіксного максимуму.
Отже піддерево природно описується функцією: dp_v(x) = максимальна кількість нових рекордів у піддереві , якщо перед входом у нього поточний максимум дорівнює .
Але зберігати цю функцію для всіх напряму не можна.
2) Який вигляд має DP-функція
Якщо збільшувати стартовий максимум , відповідь не може зростати. Тобто dp_v(x) — монотонно неспадна? Ні, саме незростаюча функція.
Більше того, вона змінюється лише в точках, пов’язаних зі значеннями з дерева. Тому її можна зберігати як набір "переломів": (x, dp_v(x)).
У коді такі стани лежать у множині cur_have або тимчасово в tmp. Причому там зберігаються лише недоміновані пари:
якщо для більшого або рівного значення не гірше, старий стан ніколи не знадобиться;
тому його можна видалити.
Саме це роблять функції ins_tmp та ins_have.
3) Що означає стан у множині
У множині зберігаються пари (x, val),
де — мінімальний потрібний стартовий максимум, а — найкраща відповідь.
Запит get\_dp\_have(v, x) означає: якщо перед піддеревом поточний максимум дорівнює , скільки нових префіксних максимумів ми ще можемо отримати?
Оскільки функція зберігається як ламана по точках перелому, достатньо знайти першу пару з ключем не меншим за :
Аналогічно працює tmp.
4) Чому дітей варто обробляти в порядку спадання mx
Перед злиттям код сортує дітей:
Це дуже важливе структурне спостереження.
Якщо ми обходимо спочатку піддерево з меншим максимальним елементом, а потім із більшим, то велике піддерево може "з’їсти" частину потенційних рекордів малого. Натомість якщо спочатку обійти піддерево з більшим mx, то після нього рекорд стане більшим, і це вже ніяк не зашкодить тим піддеревам, у яких максимуми менші — вони й так майже нічого не могли б дати після такого рекорду.
У такому DP оптимальний порядок дітей виявляється саме порядком спадання максимумів піддерев.
Це і є причина сортування.
5) Small-to-large злиття
Головна ідея реалізації — DSU on tree / small-to-large.
Для вершини вибираємо "важкого" сина big — того, у кого найбільше піддерево:
Навіщо це? Щоб не копіювати великі структури багато разів. Ми зберігаємо DP важкого сина як основний контейнер, а всі легкі піддерева по одному вливаємо в нього.
Це дає сумарну складність майже лінійно-логарифмічну.
6) Як саме зливаються піддерева
Нехай діти вже впорядковані за спаданням mx.
Тоді при побудові DP для вершини є два етапи.
Етап 1. Готуємо вклад усіх дітей, що йдуть перед big
Для легких дітей, які стоять перед big у цьому порядку, значення накопичуються в тимчасову структуру tmp.
Сенс такий: якщо ми вже обійшли кілька піддерев із більшими максимумами, то для наступного піддерева стартовий рекорд не менший за його mx[to]. Тому додатковий внесок попередніх піддерев можна витягнути через get_dp_tmp(mx[to]).
Етап 2. Беремо DP важкого сина як базу
Після цього DP важкого піддерева стає основою для всієї вершини:
Тут add[v] — лінивий зсув для всіх значень у структурі. Він означає: "до всіх відповідей цього контейнера треба додати ще стільки-то". Це дозволяє не проходити щоразу по всій множині й не збільшувати всі значення руками.
7) Навіщо потрібен add[v]
Коли ми додаємо до вже готового DP ще вклад деяких піддерев, дуже часто треба просто збільшити всю функцію на одну й ту саму константу.
Щоб не змінювати всі пари в множині, код зберігає їх у "нормалізованому" вигляді, а реальне значення повертає як значення з константою.А при вставці навпаки віднімає цей зсув.
8) Що робимо з дітьми після big
Після того як база побудована на важкому сині, у неї додаються решта дітей, які стоять після нього в порядку спадання mx.
Тут логіка та сама:
коли ми додаємо нове піддерево, усе, що вже зібрано раніше, впливає на нього лише через поточний максимум, тобто через mx[to].
9) Як додається сама вершина
Після обробки всіх дітей треба врахувати вершину .
Якщо перед входом у поточний максимум менший за , то в самій вершині ми отримуємо ще один префіксний максимум.
Що тут відбувається:
запитуємо найкращу відповідь, якщо перед входом максимум дорівнює ;
додаємо за саму вершину;
усі стани з ключем меншим за стають неактуальними, бо після відвідування максимум уже не може бути меншим за .
Це і є перехід DP для вершини.
10) Чому наприкінці відповідь це get_dp_have(root, -1)
Після компресії координат усі значення невід’ємні. Отже стартовий максимум "до початку обходу" можна вважати рівним .
Тоді: dp_{\text{root}}(-1) і є максимальною можливою кількістю префіксних максимумів у всьому обході.
13) Асимптотика
DFS для cnt та mx: .
Сортування списків дітей сумарно: O(n \log n).
Кожен DP-стан переноситься між контейнерами обмежену кількість разів завдяки small-to-large.
Сумарна складність: O(n \log^2 n) або близька до неї залежно від деталей роботи set.
Код рішення
//#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 = 2e5 + 11, inf = 1000111222;
vector <int> edge[max_n];
int c[max_n], cnt[max_n], mx[max_n];
void dfs (int v, int p = -1) {
cnt[v] = 1;
mx[v] = c[v];
for (int to : edge[v]) {
if (to == p) {
continue;
}
dfs(to, v);
cnt[v] += cnt[to];
umax(mx[v], mx[to]);
}
}
int add[max_n];
template <int lev, int n>
struct node {
};
template<int n>
struct fast_set {
int val[max_n];
// todo
};
set <pii> cur_have, tmp;
inline int get_dp_tmp (int x) {
auto it = tmp.lower_bound({x, inf});
if (it == tmp.end()) {
return 0;
}
return it->second;
}
inline void ins_tmp(int x, int val) {
auto res = tmp.insert({x, val});
if (!res.second) {
return;
}
auto it = res.first;
if (next(it) != tmp.end() && next(it)->second >= val) {
tmp.erase(it);
return;
}
while (it != tmp.begin() && prev(it)->second <= val) {
tmp.erase(prev(it));
}
}
inline int get_dp_have (int v, int x) {
auto it = cur_have.lower_bound({x, inf});
if (it == cur_have.end()) {
return 0;
}
return it->second + add[v];
}
inline void ins_have(int v, int x, int val) {
val -= add[v];
auto res = cur_have.insert({x, val});
if (!res.second) {
return;
}
auto it = res.first;
if (next(it) != cur_have.end() && next(it)->second >= val) {
cur_have.erase(it);
return;
}
while (it != cur_have.begin() && prev(it)->second <= val) {
cur_have.erase(prev(it));
}
}
vector <pii> save_val[max_n];
void solve (int v, int p = -1, int save = 0) {
sort(all(edge[v]), [&] (int a, int b) {
return mx[a] > mx[b];
});
int big = -1;
for (int to : edge[v]) {
if (to == p) {
continue;
}
if (big == -1 || cnt[to] > cnt[big]) {
big = to;
}
}
for (int to : edge[v]) {
if (to == p || to == big) {
continue;
}
solve(to, v, 0);
}
if (big != -1) {
solve(big, v, 1);
}
for (int to : edge[v]) {
if (to == p) {
continue;
}
int val = get_dp_tmp(mx[to]);
for (auto &i : save_val[to]) {
ins_tmp(i.first, i.second + val);
}
if (to == big) {
break;
}
}
if (big != -1) {
int val = get_dp_tmp(mx[big]);
swap(add[v], add[big]);
add[v] += val;
for (auto &i : tmp) {
ins_have(v, i.first, i.second);
}
tmp.clear();
}
bool ok = false;
for (int to : edge[v]) {
if (to == p) {
continue;
}
if (ok) {
int val = get_dp_have(v, mx[to]);
for (auto &i : save_val[to]) {
ins_have(v, i.first, i.second + val);
}
}
if (to == big) {
ok = true;
}
}
int val = get_dp_have(v, c[v]);
while (!cur_have.empty() && cur_have.begin()->first < c[v]) {
cur_have.erase(cur_have.begin());
}
ins_have(v, c[v], val + 1);
if (!save) {
for (auto &i : cur_have) {
save_val[v].pb({i.first, i.second + add[v]});
}
cur_have.clear();
}
}
int n;
int calc_easy (int root) {
for (int i = 0; i < n; i++) {
add[i] = 0;
cnt[i] = 0;
mx[i] = 0;
save_val[i].clear();
}
cur_have.clear();
tmp.clear();
dfs(root);
// easy(root);
solve(root, -1, 1);
int res = get_dp_have(root, -1);
return res;
}
struct State {
int mx; // поточний максимум після обходу
int cnt; // скільки префіксних максимумів вже нараховано
};
// якщо треба строго "новий рекорд" -> заміни >= на >
static inline bool is_prefix_max(int val, int curMx) {
return val > curMx;
}
State bruteDfs(int v, int p, int curMx, int curCnt) {
// відвідуємо v (pre-order)
if (is_prefix_max(c[v], curMx)) curCnt++;
curMx = max(curMx, c[v]);
// діти (сусіди без батька)
vector<int> ch;
for (int to : edge[v]) if (to != p) ch.push_back(to);
int k = (int)ch.size();
vector<char> used(k, 0);
State best{curMx, -1};
function<void(int,int,int)> go = [&](int done, int mx, int cnt) {
if (done == k) {
if (cnt > best.cnt) best = {mx, cnt};
return;
}
for (int i = 0; i < k; i++) if (!used[i]) {
used[i] = 1;
State after = bruteDfs(ch[i], v, mx, cnt);
go(done + 1, after.mx, after.cnt);
used[i] = 0;
}
};
go(0, curMx, curCnt);
return best;
}
int brute(int root) {
State res = bruteDfs(root, -1, INT_MIN, 0);
return res.cnt;
}
const int debug = 0;
mt19937 rng(228);
template<typename T = int>
inline T randll(T l = INT_MIN, T r = INT_MAX) {
return uniform_int_distribution<T>(l, r)(rng);
}
inline ld randld(ld l = INT_MIN, ld r = INT_MAX) {
return uniform_real_distribution<ld>(l, r)(rng);
}
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
ios_base::sync_with_stdio(0);
cin.tie(0);
m1:
for (int i = 0; i < n; i++) {
edge[i].clear();
}
if (!debug) {
cin >> n;
}
else {
n = randll(2, 20);
}
for (int i = 0; i < n; i++) {
if (!debug) {
cin >> c[i];
}
}
if (debug) {
for (int i = 0; i < n; i++) {
c[i] = randll(0, n);
}
// shuffle(c, c + n, rng);
}
vector <int> val(n);
for (int i = 0; i < n; i++) {
val[i] = c[i];
}
sort(all(val));
val.erase(unique(all(val)), val.end());
for (int i = 0; i < n; i++) {
c[i] = lower_bound(all(val), c[i]) - val.begin();
}
for (int i = 1, a, b; i < n; i++) {
if (!debug) {
cin >> a >> b;
--a, --b;
}
else {
a = i;
b = randll(0, i - 1);
}
edge[a].pb(b);
edge[b].pb(a);
}
int root = 0;
int ans = calc_easy(root);
cout << ans << '\n';
// LOG(n);
if (debug) {
int res = brute(root);
if (ans != res) {
exit(4);
}
goto m1;
}
}
/*
KoRoVa!
*/