zoukankan      html  css  js  c++  java
  • 【模板】【P3605】【USACO17JAN】Promotion Counting 晋升者计数——动态开点和线段树合并(树状数组/主席树)

    (题面来自Luogu)

    题目描述

    奶牛们又一次试图创建一家创业公司,还是没有从过去的经验中吸取教训--牛是可怕的管理者!

    为了方便,把奶牛从 1N(1N100,000) 编号,把公司组织成一棵树,1 号奶牛作为总裁(这棵树的根节点)。除了总裁以外的每头奶牛都有一个单独的上司(它在树上的 “双亲结点”)。所有的第 i 头牛都有一个不同的能力指数 p(i),描述了她对其工作的擅长程度。如果奶牛 i 是奶牛 的祖先节点(例如,上司的上司的上司),那么我们我们把奶牛 j 叫做 i 的下属。

    不幸地是,奶牛们发现经常发生一个上司比她的一些下属能力低的情况,在这种情况下,上司应当考虑晋升她的一些下属。你的任务是帮助奶牛弄清楚这是什么时候发生的。简而言之,对于公司的中的每一头奶牛 i,请计算其下属 j 的数量满足 p(j)>p(i)。

    输入格式

    输入的第一行包括一个整数 N。

    接下来的 N 行包括奶牛们的能力指数 p(1)p(N). 保证所有数互不相同,在区间 1⋯1e9 之间。

    接下来的 N1 行描述了奶牛 2⋯N 的上司(父节点)的编号。再次提醒,1 号奶牛作为总裁,没有上司。

    输出格式

    输出包括 N 行。输出的第 i 行应当给出有多少奶牛 i 的下属比奶牛 i 能力高。

      同样是大规模统计子树信息的问题,这题并不能拿启发式合并来做,因为子树内的要求信息和根节点的信息要求不同,与根本身的信息有关。换句话说,无法把重儿子的信息直接合并给根。

      线段树合并的一般对象是两棵动态开点的权值线段树。权值线段树动态开点可以只见出建出没有被插入过的位置,时空复杂度O(nloginf),可以节省很大的空间。查询时避开空节点即可。

    代码:

    1. void modify(int &nd, int l, int r, int x) {  
    2.     if (!nd) nd = ++tot;  
    3.     if (l == r) {  
    4.         ++cnt[nd];  
    5.         return;  
    6.     }  
    7.     if (x <= mid) modify(lc[nd], l, mid, x);  
    8.     else modify(rc[nd], mid + 1, r, x);  
    9.     update(nd);  
    10. }  

      动态开点是不需要离散化序列的,因为没有被访问到的值不会被建出来。不过在题目条件允许的情况下,离散化可以把线段树本身的空间和时间的复杂度都降到O(nlogn),而离散化本身的复杂度又很小,可以视题目尽量加上。

      线段树合并的过程很简单,它利用了动态开点线段树部分节点为空的性质,在递归的过程中直接暴力合并两棵线段树对应节点的信息,遇到空节点直接返回。合并函数写法有两种,前者通过新建线段树保留旧线段树的信息,后者直接利用原有节点,不需要额外开辟空间。静态子树统计的问题中每个节点返回后不会被再次访问,一般采用省空间的写法。

    写法1:

    1. int merge(int &u, int v) {  
    2.     if (!v) return u;  
    3.     if (!u) return v;  
    4.     int nd = ++tot;  
    5.     cnt[nd] = cnt[lc[nd]] + cnt[rc[nd]];//把v的信息并到u上  
    6.     lc[nd] = merge(lc[u], lc[v]);  
    7.     rc[nd] = merge(rc[u], rc[v]);  
    8.     return nd;  
    9. }  

    写法2:

    1. void merge(int &u, int v) {  
    2.     if (!u || !v) {//存在一个空节点  
    3.         u += v;//直接把u指向非空的那个点  
    4.         return;  
    5.     }  
    6.     seg[u].cnt += seg[v].cnt;//把v的信息并到u上  
    7.     merge(lc[u], lc[v]);  
    8.     merge(rc[u], rc[v]));  
    9.     return;  
    10. }  

      时间复杂度证明:假设最初存在的线段树共有O(nlogn)级别的节点数。每一次合并操作至少会减少一个原有节点,所以总复杂度的上界就是O(nlogn)的。

      线段树合并常数比树上启发式合并大一些,能承受的范围大概在1e6左右。不过由于每个节点合并起来得到的线段树是分开统计的,它更普适于类似的子树统计问题。

      其实该题中的查询子树权值信息和合并操作,用树状数组就可以维护。更神仙的做法是暴上主席树……但是作为线段树合并的模板还是要认真打的……

      考虑对每个叶子节点维护一棵权值线段树,然后递归地合并一个节点u的每一个子节点中的线段树,在这棵树上查询比能力值score[u]大的节点数就等价于查询[score[u] + 1, inf]的区间和。然后把score[u]插入该线段树中即可返回。由于只需要比较大小关系,可以做离散化来优化时间复杂度。

    代码:

    1. #include <iostream>  
    2. #include <cstdio>  
    3. #include <cstring>  
    4. #include <algorithm>  
    5. #define BUG puts("$$$")  
    6. #define maxn 100010  
    7. template <typename T>   
    8. void read(T &x) {  
    9.     x = 0;  
    10.     char ch = getchar();  
    11. //  int f = 1;  
    12.     while (!isdigit(ch)) {  
    13. //      if (ch == '-') f = -1;   
    14.         ch = getchar();  
    15.     }  
    16.     while (isdigit(ch)) {  
    17.         x = x * 10 + (ch ^ 48);  
    18.         ch = getchar();  
    19.     }  
    20. //  x *= f;  
    21. }  
    22. using namespace std;  
    23. int n, N;  
    24. int head[maxn], top;   
    25. struct E {  
    26.     int to, nxt;  
    27. } edge[maxn << 1];  
    28. inline void insert(int u, int v) {  
    29.     edge[++top] = (E) {v, head[u]};  
    30.     head[u] = top;  
    31. }  
    32. int score[maxn];  
    33. namespace Segment_tree {  
    34.     #define mid ((l + r) >> 1)  
    35.     int tot = 0;  
    36.     struct node {  
    37.         int cnt, lc, rc;  
    38.         node(): cnt(0), lc(0), rc(0) {}  
    39.     } seg[maxn << 4];  
    40.     #define lc(nd) seg[nd].lc  
    41.     #define rc(nd) seg[nd].rc  
    42.     void update(int nd) {  
    43.         seg[nd].cnt = seg[lc(nd)].cnt + seg[rc(nd)].cnt;  
    44.     }  
    45.     void modify(int &nd, int l, int r, int x) {  
    46.         if (!nd) nd = ++tot;  
    47.         if (l == r) {  
    48.             ++seg[nd].cnt;  
    49.             return;  
    50.         }  
    51.         if (x <= mid) modify(lc(nd), l, mid, x);  
    52.         else modify(rc(nd), mid + 1, r, x);  
    53.         update(nd);  
    54.     }  
    55.     int query(int nd, int l, int r, int ql, int qr) {  
    56.         if (!nd) return 0;  
    57.         if (l >= ql && r <= qr)  
    58.             return seg[nd].cnt;  
    59.         if (l > qr || r < ql)  
    60.             return 0;  
    61.         return query(lc(nd), l, mid, ql, qr) + query(rc(nd), mid + 1, r, ql, qr);  
    62.     }  
    63.     void merge(int &u, int v) {  
    64.         if (!u || !v) {  
    65.             u += v;  
    66.             return;  
    67.         }  
    68.         seg[u].cnt += seg[v].cnt;  
    69.         merge(lc(u), lc(v));  
    70.         merge(rc(u), rc(v));  
    71.         return;  
    72.     }  
    73.     int ans[maxn], rt[maxn];  
    74.     void solve(int u) {//核心代码
    75.         for (int i = head[u]; i; i = edge[i].nxt) {  
    76.             int v = edge[i].to;  
    77.             solve(v);  
    78.             merge(rt[u], rt[v]);  
    79.         }  
    80.         ans[u] = query(rt[u], 1, N, score[u] + 1, N);  
    81.         modify(rt[u], 1, N, score[u]);  
    82.         return;  
    83.     }  
    84. using namespace Segment_tree;  
    85. int st[maxn];  
    86. int contra(int *a) {//离散化
    87.     memcpy(st, a, sizeof(st));  
    88.     sort(st + 1, st + 1 + n);  
    89.     int len = unique(st + 1, st + 1 + n) - st - 1;  
    90.     for (int i = 1; i <= n; ++i)  
    91.         a[i] = lower_bound(st + 1, st + 1 + len, a[i]) - st;  
    92.     return len;  
    93. }  
    94.   
    95. int main() {  
    96.     read(n);  
    97.     int u, v;  
    98.     for (int i = 1; i <= n; ++i)  
    99.         read(score[i]);  
    100.     N = contra(score);  
    101.     for (u = 2; u <= n; ++u) {  
    102.         read(v);  
    103.         insert(v, u);  
    104.     }  
    105.     solve(1);  
    106.     for (int i = 1; i <= n; ++i)  
    107.         printf("%d ", ans[i]);  
    108.     return 0;  
    109. }  
  • 相关阅读:
    决策树算法之ID3
    MSE与MAE的区别与选择
    (九)关键变量发掘技术
    (八)数据集切割
    (七)数据精简之(数据值精简和字段的精简)
    (六)数据精简之(数据记录精简)
    (五)数据编码是干什么
    (四)数据清洗主要工作
    pip-window安装
    CMDB学习之六 --客户端请求测试,服务端api优化
  • 原文地址:https://www.cnblogs.com/TY02/p/11349771.html
Copyright © 2011-2022 走看看