Description

给定一棵树,每个点有一个权值,求每个点所有后代中权值比它大的点的个数。

$n\leq10^5$

不怎么难,就是看对数据结构的掌握程度。。

算法一

对每个点维护一个权值线段树,从下往上合并,每次合并该结点所有儿子即可。$O(n\log n)$​​,时间空间常数都很大。

Code

我觉得指针比数组清晰一些,虽然也伴随着一点小风险(访问空指针)。

代码还是很清晰的,不难写。

#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
using std::cout;
using std::endl;

const int maxn = 1e5 + 10;

struct Node
{
    int sum;
    Node *ls, *rs;
    Node() : sum(0), ls(nullptr), rs(nullptr) {}
    void push_up()
    {
        sum = (ls == nullptr ? 0 : ls->sum) + (rs == nullptr ? 0 : rs->sum);
    }
} a[maxn << 5], *root[maxn];
int tot;

Node *modify(int l, int r, int x, Node *p)
{
    if (p == nullptr)
        p = &a[++tot];
    if (l == r)
    {
        (p->sum)++;
        return p;
    }
    int mid = (l + r) / 2;
    if (x <= mid)
        p->ls = modify(l, mid, x, p->ls);
    else
        p->rs = modify(mid + 1, r, x, p->rs);
    p->push_up();
    return p;
}

int query(int l, int r, int x, int y, Node *p)
{
    if (p == nullptr)
        return 0;
    if (l >= x && r <= y)
        return p->sum;
    int mid = (l + r) / 2;
    int res = 0;
    if (x <= mid)
        res += query(l, mid, x, y, p->ls);
    if (y > mid)
        res += query(mid + 1, r, x, y, p->rs);
    return res;
}

Node *merge(int l, int r, Node *a, Node *b)
{
    if (a == nullptr)
        return b;
    if (b == nullptr)
        return a;
    if (l == r)
    {
        a->sum += b->sum;
        return a;
    }
    int mid = (l + r) / 2;
    a->ls = merge(l, mid, a->ls, b->ls), a->rs = merge(mid + 1, r, a->rs, b->rs);
    a->push_up();
    return a;
}

int ans[maxn], val[maxn], b[maxn], n;
std::vector<int> e[maxn];
inline void add(int x, int y) { e[x].push_back(y); }

void dfs(int x)
{
    for (int v : e[x])
    {
        dfs(v);
        root[x] = merge(1, n, root[x], root[v]);
    }
    ans[x] = query(1, n, val[x] + 1, n, root[x]);
    root[x] = modify(1, n, val[x], root[x]);
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &val[i]), b[i] = val[i];
    for (int i = 2, x; i <= n; i++)
        scanf("%d", &x), add(x, i);
    std::sort(b + 1, b + 1 + n);
    for (int i = 1; i <= n; i++)
        root[i] = &a[++tot], val[i] = std::lower_bound(b + 1, b + 1 + n, val[i]) - b;
    dfs(1);
    for (int i = 1; i <= n; i++)
        printf("%d\n", ans[i]);
}

算法二

对每个点维护一个平衡树,然后从下向上启发式合并,对每个点查询排名,减一下就是答案。$O(n\log^2n)$​​,虽然复杂度比较高但常数很小,实际上比上面的线段树合并快 $\text{70ms}$​。空间没啥常数。

Code

忽然想起 pb_ds,稍微学了点写了一些。不得不说是真省事,而且红黑树也是真快。(我估计比不少手写 splay/treap 的快)

这代码就更清晰了()

#include <algorithm>
#include <cstdio>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
#include <iostream>
#include <set>
using std::cout;
using std::endl;

const int maxn = 2e5 + 10;

inline bool cmp(int x, int y) { return x < y; }

__gnu_pbds::tree<int, __gnu_pbds::null_type, std::less<int>,
                 __gnu_pbds::rb_tree_tag,
                 __gnu_pbds::tree_order_statistics_node_update>
    S[maxn], *root[maxn];

std::vector<int> e[maxn];
inline void add(int x, int y) { e[x].push_back(y); }

int n, a[maxn], ans[maxn];

void dfs(int x)
{
    auto mx = root[x];
    for (int v : e[x])
    {
        dfs(v);
        if (root[v]->size() > mx->size())
            mx = root[v];
    }
    root[x] = mx;
    for (int v : e[x])
    {
        if (root[v] != root[x])
        {
            for (int it : *root[v])
                root[x]->insert(it);
            root[v]->clear();
        }
    }
    ans[x] = root[x]->size() - root[x]->order_of_key(a[x]);
    root[x]->insert(a[x]);
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]), root[i] = &S[i];
    for (int i = 2, x; i <= n; i++)
        scanf("%d", &x), add(x, i);
    dfs(1);
    for (int i = 1; i <= n; i++)
        printf("%d\n", ans[i]);
}

算法三

搞个 dfs 序出来,然后就变成序列题目,可以用类似逆序对的方法做,树状数组常数很小。

或者分块,我还看到有主席树的qwq。