题解:AtCoder AT_awc0082_e Company Organization and Salaries

本文分享的必刷题目是从蓝桥云课洛谷AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。

欢迎大家订阅我的专栏:算法题解:C++与Python实现

附上汇总贴:算法竞赛备考冲刺必刷题(C++) | 汇总


【题目来源】

AtCoder:E - Company Organization and Salaries

【题目描述】

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

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

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

Each employee i i i has a positive integer value A i A_i Ai representing their salary.

For organizational analysis, Takahashi needs to process a total of Q Q 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 v v to x x x.

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

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

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

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

每名员工 i i i 有一个正整数 A i A_i Ai 表示其工资。

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

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

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

【输入】

N N N Q Q Q
A 1 A_1 A1 A 2 A_2 A2 ⋯ \cdots A N A_N AN
P 2 P_2 P2 P 3 P_3 P3 ⋯ \cdots P N P_N PN
query 1 \text{query}_1 query1
query 2 \text{query}_2 query2
⋮ \vdots
query Q \text{query}_Q queryQ

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

【输出】

For each Query 2, output on a single line the number of subordinates of employee v v v whose salary is strictly greater than the salary of employee v v v. If employee v v v has no subordinates, output 0 0 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 N N 名员工的有根树结构,每名员工有工资 A i A_i Ai。进行 Q Q Q 次操作:操作 1 1 1 修改员工 v v v 的工资为 x x x;操作 2 2 2 查询员工 v v v 的所有下属中工资严格大于 A [ v ] A[v] A[v] 的人数。这是一个**线段树套树状数组(动态开点线段树 / 树套树)**问题,关键在于将子树查询转化为区间查询,并支持动态值域统计。

  2. 算法选择

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

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

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

    • 二维数据结构:外层线段树维护一维信息(DFS 序区间),内层树状数组维护另一维信息(值域),实现二维区间统计
    • 子树转区间:利用 DFS 序将有根树的子树转化为连续区间,将复杂的树形查询转化为简单的区间查询
    • 离散化优化空间:对每个线段树节点单独离散化,只存储该区间内可能出现的值,大幅减少空间开销
    • 离线预处理:先读入所有操作,收集所有可能出现的值,确保离散化包含全部可能的状态,避免动态开点的复杂实现
    • 值域统计查询:查询"值大于 A [ v ] A[v] 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值