本文分享的必刷题目是从蓝桥云课、洛谷、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 2≤i≤N) 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 2≤i≤N)恰好有一名直接上司 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
【核心思想】
-
问题分析:给定 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] 的人数。这是一个**线段树套树状数组(动态开点线段树 / 树套树)**问题,关键在于将子树查询转化为区间查询,并支持动态值域统计。
-
算法选择:
- 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):先读入所有操作,收集所有可能出现的值,再进行离散化和建树
-
关键步骤:
- 初始化:
- 读取 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),将初始工资加入数据结构
- 对每个员工
i
i
i,执行
- 处理查询(共
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
- 修改操作
- 初始化:
-
时间/空间复杂度:
- 时间复杂度: 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) 个线段树节点中
-
线段树套树状数组的核心思想:
- 二维数据结构:外层线段树维护一维信息(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
458

被折叠的 条评论
为什么被折叠?



