І знову дерева#12372

І знову дерева (Розбір)

Автор задачі:

Андрій Холод (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!
*/