题解:AtCoder AT_awc0082_e Company Organization and Salaries

【题目来源】

AtCoder:E - Company Organization and Salaries

【题目描述】

Takahashi is a data analyst working in the human resources department of a company. The company has \(N\) employees, each assigned an employee number from \(1\) to \(N\).

In this company, there exist boss-subordinate relationships among employees. Each employee \(i\) (\(2 \leq i \leq N\)) has exactly one direct boss \(P_i\), and it is guaranteed that \(P_i < i\). Employee \(1\) is the president and has no direct boss. This relationship forms a rooted tree with employee \(1\) as the root.

Employee \(u\) is a subordinate of employee \(v\) if \(u\) is a descendant of \(v\) in the rooted tree. In other words, \(v\) is not on the path from \(v\) to the root that includes \(u\), but \(v\) is on the path from \(u\) to the root. More precisely, \(u\) is a subordinate of \(v\) if and only if employee \(v\) can be reached from employee \(u\) by following the direct boss relationship one or more times. Note that employee \(v\) itself is not considered a subordinate of employee \(v\).

Each employee \(i\) has a positive integer value \(A_i\) representing their salary.

For organizational analysis, Takahashi needs to process a total of \(Q\) queries of the following two types. Each query is processed sequentially in the given order, and changes made by update queries are reflected in all subsequent queries.

Query 1 (Update): 1 v x — Change the salary of employee \(v\) to \(x\).

Query 2 (Investigation): 2 v — Among all subordinates of employee \(v\), find and output the number of those whose salary is strictly greater than the salary of employee \(v\) at the time the query is processed.

高桥是一家公司人力资源部的数据分析师。公司有 \(N\) 名员工,每人分配了从 \(1\)\(N\) 的员工编号。

在这家公司中,员工之间存在上司-下属关系。每名员工 \(i\)\(2 \leq i \leq N\))恰好有一名直接上司 \(P_i\),并且保证 \(P_i < i\)。员工 \(1\) 是总裁,没有直接上司。这种关系形成了一棵以员工 \(1\) 为根的有根树。

员工 \(u\) 是员工 \(v\)下属,当且仅当 \(u\) 是有根树中 \(v\) 的后代。换句话说,\(v\) 不在从 \(v\) 到根的包含 \(u\) 的路径上,但 \(v\) 在从 \(u\) 到根的路径上。更准确地说,\(u\)\(v\) 的下属,当且仅当从员工 \(u\) 出发,沿着直接上司关系一次或多次可以到达员工 \(v\)。注意,员工 \(v\) 本身不被视为 \(v\) 的下属。

每名员工 \(i\) 有一个正整数 \(A_i\) 表示其工资。

为了进行组织分析,高桥需要处理总共 \(Q\) 个以下两种类型的查询。每个查询按给定顺序依次处理,更新查询所做的更改会反映在所有后续查询中。

查询 1(更新):1 v x — 将员工 \(v\) 的工资更改为 \(x\)

查询 2(调查):2 v — 在员工 \(v\) 的所有下属中,找出并输出那些在处理查询时工资严格大于员工 \(v\) 工资的下属人数。

【输入】

\(N\) \(Q\)
\(A_1\) \(A_2\) \(\cdots\) \(A_N\)
\(P_2\) \(P_3\) \(\cdots\) \(P_N\)
\(\text{query}_1\)
\(\text{query}_2\)
\(\vdots\)
\(\text{query}_Q\)

  • The first line contains the integer \(N\) representing the number of employees and the integer \(Q\) representing the number of queries, separated by a space.
  • The second line contains the initial salaries \(A_1, A_2, \ldots, A_N\) of each employee, separated by spaces.
  • The third line contains the integers \(P_2, P_3, \ldots, P_N\) representing the direct bosses of employees \(2\) through \(N\), separated by spaces. When \(N = 1\), this line is empty (an empty line is given).
  • The following \(Q\) lines each contain one query.
  • For Query 1: Given in the format 1 v x, where the employee number \(v\) and the new salary \(x\) are separated by spaces.
  • For Query 2: Given in the format 2 v, where the employee number \(v\) is given.

【输出】

For each Query 2, output on a single line the number of subordinates of employee \(v\) whose salary is strictly greater than the salary of employee \(v\). If employee \(v\) has no subordinates, output \(0\).

【输入样例】

5 5
10 5 15 8 3
1 1 2 2
2 1
2 2
1 5 20
2 2
2 1

【输出样例】

1
1
2
2

【核心思想】

  1. 问题分析:给定 \(N\) 名员工的有根树结构,每名员工有工资 \(A_i\)。进行 \(Q\) 次操作:操作 \(1\) 修改员工 \(v\) 的工资为 \(x\);操作 \(2\) 查询员工 \(v\) 的所有下属中工资严格大于 \(A[v]\) 的人数。这是一个线段树套树状数组(动态开点线段树 / 树套树)问题,关键在于将子树查询转化为区间查询,并支持动态值域统计。

  2. 算法选择

    • DFS 序(欧拉序):将有根树的子树转化为连续区间 \([In[v], Out[v]]\),子树查询变为区间查询
    • 线段树套树状数组(Segment Tree of Fenwick Trees):外层线段树维护 DFS 序区间,内层树状数组维护值域分布,支持区间值域统计
    • 离散化(Coordinate Compression):对所有可能出现的工资值进行离散化,减少树状数组空间
    • 离线处理(Offline Processing):先读入所有操作,收集所有可能出现的值,再进行离散化和建树
  3. 关键步骤

    • 初始化
      • 读取 \(N\)(员工数量)、\(Q\)(查询数量)
      • 读取初始工资 \(A[1..N]\) 和父子关系,构建树的邻接表
      • 读入所有 \(Q\) 个查询,记录操作类型、员工编号、新工资(如果是修改操作)
    • DFS 求欧拉序
      • 从根节点 \(1\) 开始 DFS,记录每个节点的进入时间 \(In[v]\) 和离开时间 \(Out[v]\)
      • 子树 \(v\) 对应 DFS 序区间 \([In[v], Out[v]]\)
    • 离散化处理
      • 收集所有初始工资和所有修改操作中的新工资
      • 对每个线段树节点,收集该节点区间内所有可能出现的值,排序去重
    • 构建线段树套树状数组
      • 自底向上构建线段树,每个节点维护一个离散化后的树状数组
      • 树状数组存储该区间内各工资值的出现次数
    • 初始化数据
      • 对每个员工 \(i\),执行 update(In[i], A[i], 1),将初始工资加入数据结构
    • 处理查询(共 \(Q\) 次):
      • 修改操作 1 v x
        • update(In[v], A[v], -1):删除旧工资
        • A[v] = x:更新工资值
        • update(In[v], A[v], +1):加入新工资
      • 查询操作 2 v
        • 查询区间 \([In[v], Out[v]]\)(子树 \(v\))中值在 \([A[v]+1, \infty)\) 范围内的个数
        • ans = query(In[v], Out[v], A[v]+1, INF)
        • 输出 ans
  4. 时间/空间复杂度

    • 时间复杂度:\(O((N + Q) \log^2 N)\),每次修改/查询需要遍历 \(O(\log N)\) 个线段树节点,每个节点内进行 \(O(\log N)\) 的树状数组操作
    • 空间复杂度:\(O(N \log N)\),每个值出现在 \(O(\log N)\) 个线段树节点中
  5. 线段树套树状数组的核心思想

    • 二维数据结构:外层线段树维护一维信息(DFS 序区间),内层树状数组维护另一维信息(值域),实现二维区间统计
    • 子树转区间:利用 DFS 序将有根树的子树转化为连续区间,将复杂的树形查询转化为简单的区间查询
    • 离散化优化空间:对每个线段树节点单独离散化,只存储该区间内可能出现的值,大幅减少空间开销
    • 离线预处理:先读入所有操作,收集所有可能出现的值,确保离散化包含全部可能的状态,避免动态开点的复杂实现
    • 值域统计查询:查询"值大于 \(A[v]\) 的个数"转化为值域区间查询,利用树状数组的前缀和性质快速计算
    • 适用于"树上子树查询 + 值域统计 + 动态修改"类问题

【算法标签】

线段树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
const int SZ = 1 << 17;  // 线段树大小(大于N的最小2的幂)

// ==================== 树状数组模板(值域离散化版) ====================
struct Fenwick {
    vector<int> coord;      // 离散化后的坐标
    vector<int> tree;       // 树状数组
    Fenwick() {}
    Fenwick(const vector<int>& c) : coord(c) {  // 构造函数
        tree.resize(coord.size() + 5);  // 初始化树状数组
    }
    // 在值x的位置加上v
    void add(int x, int v) {
        int pos = lower_bound(coord.begin(), coord.end(), x) - coord.begin() + 1;  // 获取离散化位置
        for (int i = pos; i < tree.size(); i += i & -i) {  // 树状数组更新
            tree[i] += v;
        }
    }
    // 查询前缀和[1..pos]
    int query(int pos) const {
        int res = 0;
        for (int i = pos; i > 0; i -= i & -i) {  // 树状数组查询
            res += tree[i];
        }
        return res;
    }
    // 查询值域[l, r]的和
    int rangeQuery(int l, int r) const {
        l = lower_bound(coord.begin(), coord.end(), l) - coord.begin();  // 左边界位置
        r = upper_bound(coord.begin(), coord.end(), r) - coord.begin() - 1;  // 右边界位置
        if (l > r) return 0;  // 无效区间
        return query(r + 1) - (l > 0 ? query(l) : 0);  // 前缀和相减
    }
};

// ==================== 线段树套树状数组 ====================
vector<int> coord[SZ << 1];     // 每个线段树节点的离散化坐标
Fenwick bit[SZ << 1];           // 每个线段树节点的树状数组
// 向线段树节点x注册一个值v
void registerVal(int x, int v) {
    coord[x | SZ].push_back(v);  // 在叶子节点注册值
}
// 构建线段树:自底向上合并坐标并建立树状数组
void build() {
    // 叶子节点:排序去重
    for (int i = SZ; i < SZ * 2; i++) {
        auto& c = coord[i];
        sort(c.begin(), c.end());  // 排序
        c.erase(unique(c.begin(), c.end()), c.end());  // 去重
    }
    // 内部节点:合并子节点坐标
    for (int i = SZ - 1; i > 0; i--) {
        auto& c = coord[i];
        c.resize(coord[i << 1].size() + coord[i << 1 | 1].size());  // 调整大小
        merge(coord[i << 1].begin(), coord[i << 1].end(),
              coord[i << 1 | 1].begin(), coord[i << 1 | 1].end(),
              c.begin());  // 合并
        c.erase(unique(c.begin(), c.end()), c.end());  // 去重
    }
    // 建立树状数组
    for (int i = 1; i < SZ * 2; i++) {
        bit[i] = Fenwick(coord[i]);  // 为每个节点建立树状数组
    }
}
// 单点修改:在位置x(DFS序)的值y处加上v
void update(int x, int y, int v) {
    for (x |= SZ; x; x >>= 1) {  // 向上更新
        bit[x].add(y, v);
    }
}
// 区间查询:查询DFS序区间[s, e]中值在[l, r]范围内的个数
int query(int s, int e, int l, int r) {
    int res = 0;
    for (s |= SZ, e |= SZ; s <= e; s >>= 1, e >>= 1) {  // 线段树查询
        if (s & 1) res += bit[s++].rangeQuery(l, r);  // 左节点
        if (!(e & 1)) res += bit[e--].rangeQuery(l, r);  // 右节点
    }
    return res;
}

// ==================== 主程序 ====================
vector<int> adj[N];
int A[N], In[N], Out[N];
int op[N], V[N], X[N];
int n, q, timerDFS = 0;
// DFS求入时间和出时间
void dfs(int u) {
    In[u] = ++timerDFS;  // 记录进入时间
    for (int v : adj[u]) dfs(v);  // 递归子节点
    Out[u] = timerDFS;  // 记录离开时间
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> q;  // 输入节点数和查询数
    for (int i = 1; i <= n; i++) cin >> A[i];  // 输入节点值
    for (int i = 2; i <= n; i++) {
        int p; cin >> p;
        adj[p].push_back(i);  // 构建树
    }
    // 读入所有查询
    for (int i = 1; i <= q; i++) {
        cin >> op[i] >> V[i];
        if (op[i] == 1) cin >> X[i];  // 修改操作需要新值
    }
    dfs(1);  // DFS得到欧拉序
    // 收集所有可能出现的值(初始值 + 修改后的值)
    for (int i = 1; i <= n; i++) {
        registerVal(In[i], A[i]);  // 注册初始值
    }
    for (int i = 1; i <= q; i++) {
        if (op[i] == 1) registerVal(In[V[i]], X[i]);  // 注册修改值
    }
    build();  // 构建线段树套树状数组
    // 初始化:加入所有初始值
    for (int i = 1; i <= n; i++) {
        update(In[i], A[i], 1);
    }
    // 处理查询
    for (int i = 1; i <= q; i++) {
        int v = V[i];
        if (op[i] == 1) {
            // 修改:先删除旧值,再加入新值
            update(In[v], A[v], -1);
            A[v] = X[i];
            update(In[v], A[v], +1);
        } else {
            // 查询:在v的子树中找大于A[v]的个数
            // 子树区间 [In[v], Out[v]],值域 [A[v]+1, INF]
            cout << query(In[v], Out[v], A[v] + 1, 1e9) << "\n";
        }
    }
    return 0;
}

【运行结果】

5 5
10 5 15 8 3
1 1 2 2
2 1
2 2
1 5 20
2 2
2 1
1
1
2
2
posted @ 2026-06-15 09:50  团爸讲算法  阅读(5)  评论(0)    收藏  举报