更多 CSP 认证考试题目题解可以前往:CSP-CCF 认证考试真题题解
原题链接: 202403-5 文件夹合并
时间限制: 2.0 秒
空间限制: 512 MiB
题目描述
新入职西西艾弗岛有限公司的小 C 接替了刚刚升职的小 S 的项目。然而小 C 打开项目工程时,一层层嵌套的文件夹让小 C 感到眼花缭乱。为了精简项目结构,小 C 决定对项目的文件夹进行一些必要的合并。
项目中共有 n n n 个文件夹。为了方便,我们用 1 1 1 至 n n n 的整数给这 n n n 个文件夹编号,其中编号为 1 1 1 的文件夹为项目的根文件夹,其他每个文件夹都有一个父文件夹,这些文件夹构成了树形结构。除了子文件夹以外,第 i i i 个文件夹内还直接存储了 d i d_i di 字节的数据。
小 C 进行了若干次文件夹合并操作。每次操作中小 C 会选择一个文件夹 x j x_j xj,将这个文件夹和它的所有子文件夹合并。具体地,小 C 会进行以下操作:遍历 x j x_j xj 的子文件夹 y y y,将文件夹 y y y 包含的所有文件夹和文件移动到文件夹 x j x_j xj,然后删除文件夹 y y y。所有文件和文件夹的名称是两两不同的,合并过程中不需要考虑文件或文件夹重名的情况。在每一次合并操作后,小 C 需要知道文件夹 x j x_j xj 内共有几个文件夹以及多少字节的数据。
例如,考虑以下项目:根文件夹内有文件夹 2 2 2 和文件夹 3 3 3 以及 100 100 100 字节数据,其中文件夹 2 2 2 为空文件夹,文件夹 3 3 3 内有 200 200 200 字节数据和文件夹 4 4 4,文件夹 4 4 4 包含 300 300 300 字节数据。对根文件夹进行一次合并后,文件夹 2 2 2 和文件夹 3 3 3 被合并至根文件夹,此时根文件夹下有文件夹 4 4 4 以及 300 300 300 字节数据,而文件夹 4 4 4 下也包含 300 300 300 字节数据。
在合并文件夹的过程中,小 C 常常需要访问某个文件夹 z j z_j zj 下的文件。此时,小 C 会从根文件夹开始,每次进入当前文件夹的一个子文件夹。小 C 需要知道按照以上过程,获取到文件夹 z j z_j zj 下的文件至少需要经过多少个文件夹。
例如,在以上项目中,未对根文件夹进行合并前,访问根文件夹下的文件只需要经过根文件夹一个文件夹,而访问文件夹 4 4 4 则需要经过根文件夹以及文件夹 3 3 3 和 4 4 4。而对根文件夹进行合并之后,访问文件夹 4 4 4 只需要经过根文件夹和文件夹 4 4 4 了。
在整个项目中,小 C 一共进行了 m m m 次文件夹合并以及文件访问操作。你需要帮助小 C 正确维护文件夹之间的关系,并在每次操作后正确回答小 C 需要的数据。
输入格式
从标准输入读入数据。
输入的第一行两个整数 n , m n,m n,m,分别表示文件夹数量以及操作次数。
第二行 ( n − 1 ) (n-1) (n−1) 个整数 f 2 , ⋯ , f n f_2,\cdots,f_n f2,⋯,fn,其中 f i f_i fi 表示文件夹 i i i 的父文件夹编号。
第三行 n n n 个整数 d 1 , d 2 , ⋯ , d n d_1,d_2,\cdots,d_n d1,d2,⋯,dn,其中 d i d_i di 表示文件夹 i i i 中数据的存储量。
接下来 m m m 行第 j j j 行两个整数,第一个整数 o p j op_j opj 表示操作类型。若 o p j = 1 op_j = 1 opj=1 则表示一次文件夹合并操作,接下来一个整数 x j x_j xj 表示合并的文件夹编号;若 o p j = 2 op_j = 2 opj=2 则表示一次文件访问操作,接下来一个整数 z j z_j zj 表示访问的文件夹编号。
输出格式
输出到标准输出。
输出 m m m 行,第 j j j 行表示第 j j j 个操作中小 C 需要的数据:若 o p j = 1 op_j=1 opj=1 则输出两个整数,依次表示文件夹 x j x_j xj 的子文件夹数量以及数据的存储量;若 o p j = 2 op_j = 2 opj=2 则输出一个整数表示小 C 获取文件夹 z j z_j zj 下的数据最少需要经过的文件夹个数。
样例1输入
4 6
1 1 3
100 0 200 300
2 1
2 4
1 1
2 4
1 1
1 1
样例1输出
1
3
1 300
2
0 600
0 600
子任务
对于所有测试数据,
- 1 ≤ n ≤ 5 × 1 0 5 , 1 ≤ m ≤ 3 × n 1 \leq n \leq 5 \times 10^{5}, 1 \leq m \leq 3 \times n 1≤n≤5×105,1≤m≤3×n,
- 1 ≤ f i ≤ n 1 \leq f_i \leq n 1≤fi≤n,输入的文件夹结构构成树形结构,
- 0 ≤ d i ≤ 1 0 5 0 \leq d_i \leq 10^{5} 0≤di≤105,
- 1 ≤ x j , z j ≤ n 1 \leq x_j, z_j \leq n 1≤xj,zj≤n,每次合并操作中给出的文件夹 x j x_j xj 没有被删除,每次文件访问操作中给出的文件夹 z j z_j zj 没有被删除。
子任务编号 | n ≤ n \le n≤ | 特殊性质 | 分值 |
---|---|---|---|
1 | 500 500 500 | 无 | 10 |
2 | 5 , 000 5,000 5,000 | 无 | 15 |
3 | 1 0 5 10^{5} 105 | 无 | 15 |
4 | 5 × 1 0 5 5 \times 10^{5} 5×105 | A | 5 |
5 | 5 × 1 0 5 5 \times 10^{5} 5×105 | B | 5 |
6 | 5 × 1 0 5 5 \times 10^{5} 5×105 | C | 10 |
7 | 5 × 1 0 5 5 \times 10^{5} 5×105 | D | 15 |
8 | 5 × 1 0 5 5 \times 10^{5} 5×105 | E | 10 |
9 | 5 × 1 0 5 5 \times 10^{5} 5×105 | 无 | 15 |
特殊性质 A: f i = ( i − 1 ) f_i = (i-1) fi=(i−1)。
特殊性质 B: f i = 1 f_i = 1 fi=1。
特殊性质 C:在文件夹合并操作中, x j = 1 x_j = 1 xj=1。
特殊性质 D: o p j = 1 op_j = 1 opj=1,即没有文件访问操作。
特殊性质 E: o p j = 2 op_j = 2 opj=2,即没有文件夹合并操作。
题解
维护子文件夹数量以及数据的存储量:对于合并文件夹操作 x x x,相当于把 x x x 所有儿子的儿子全部挂到 x x x 下,并把 x x x 的儿子的数据大小加到 x x x 中。这里使用链表来进行操作,对 x x x 的每个儿子只需要 O ( 1 ) \mathcal{O}(1) O(1) 的复杂度,从总体上看,一个节点最多被删 1 1 1 次,总的复杂度不会超过节点个数 O ( n ) \mathcal{O}(n) O(n)。
维护获取文件夹下的数据最少需要经过的文件夹个数:等价于求该文件夹的深度。使用 dfs 序将所有节点进行重新编号,编号后,对于一个文件夹的所有子文件夹,dfs 序是连续的,记 i i i 文件夹的 dfs 序为 d f n i dfn_i dfni。预处理出每个文件夹的子文件夹的最小和最大 dfs 序,记为 l i , r i l_i,r_i li,ri。对于合并文件夹操作 x x x,相当于把 x x x 文件夹的所有子文件夹深度 − 1 -1 −1,即将 [ l i , r i ] [l_i,r_i] [li,ri] 中的数 − 1 -1 −1(由于不存在访问已经删除的文件夹,所有可以忽略这些文件夹的处理,直接进行区间减)。对于文件夹访问操作 z z z,直接查询该文件夹深度即可。这是一个经典的区间修改、单点查询问题,写棵线段树即可。
时间复杂度: O ( m log n ) \mathcal{O}(m\log n) O(mlogn)。
参考代码
/*
Created by Pujx on 2024/5/8.
*/
#pragma GCC optimize(2, 3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
//#define int long long
//#define double long double
using i64 = long long;
using ui64 = unsigned long long;
using i128 = __int128;
#define inf (int)0x3f3f3f3f3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define yn(x) cout << (x ? "yes" : "no") << endl
#define Yn(x) cout << (x ? "Yes" : "No") << endl
#define YN(x) cout << (x ? "YES" : "NO") << endl
#define mem(x, i) memset(x, i, sizeof(x))
#define cinarr(a, n) for (int _ = 1; _ <= n; _++) cin >> a[_]
#define cinstl(a) for (auto& _ : a) cin >> _
#define coutarr(a, n) for (int _ = 1; _ <= n; _++) cout << a[_] << " \n"[_ == n]
#define coutstl(a) for (const auto& _ : a) cout << _ << ' '; cout << endl
#define all(x) (x).begin(), (x).end()
#define md(x) (((x) % mod + mod) % mod)
#define ls (s << 1)
#define rs (s << 1 | 1)
#define ft first
#define se second
#define pii pair<int, int>
#ifdef DEBUG
#include "debug.h"
#else
#define dbg(...) void(0)
#endif
const int N = 5e5 + 5;
//const int M = 1e5 + 5;
const int mod = 998244353;
//const int mod = 1e9 + 7;
//template <typename T> T ksm(T a, i64 b) { T ans = 1; for (; b; a = 1ll * a * a, b >>= 1) if (b & 1) ans = 1ll * ans * a; return ans; }
//template <typename T> T ksm(T a, i64 b, T m = mod) { T ans = 1; for (; b; a = 1ll * a * a % m, b >>= 1) if (b & 1) ans = 1ll * ans * a % m; return ans; }
int a[N];
int n, m, t, k, q;
int fa[N];
int head[N], tail[N], sz[N], to[N], nxt[N], cnt;
i64 d[N];
void add(int u, int v) {
++cnt;
if (!sz[u]) tail[u] = cnt;
to[cnt] = v;
nxt[cnt] = head[u];
head[u] = cnt;
sz[u]++;
}
void merge(int u, int v) {
if (!sz[v]) return;
if (!sz[u]) head[u] = head[v];
else nxt[tail[u]] = head[v];
tail[u] = tail[v];
sz[u] += sz[v];
}
int dep[N], l[N], r[N], tot;
vector<int> dfn;
void dfs(int u) {
l[u] = ++tot;
dfn.emplace_back(u);
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
dep[v] = dep[u] + 1;
dfs(v);
}
r[u] = tot;
}
template <typename T> struct SegmentTree {
struct TreeNode { int l, r; T add, st, sum, mx, mn; } tr[N << 2];
void pushup(int s) {
tr[s].sum = tr[ls].sum + tr[rs].sum;
tr[s].mx = max(tr[ls].mx, tr[rs].mx);
tr[s].mn = min(tr[ls].mn, tr[rs].mn);
}
void pushdown(int s) {
if (tr[s].st != numeric_limits<T>::min()) {
tr[s].st += tr[s].add;
tr[ls].add = tr[rs].add = 0;
tr[ls].st = tr[rs].st = tr[s].st;
tr[ls].sum = tr[s].st * (tr[ls].r - tr[ls].l + 1);
tr[rs].sum = tr[s].st * (tr[rs].r - tr[rs].l + 1);
tr[ls].mx = tr[rs].mx = tr[s].st;
tr[ls].mn = tr[rs].mn = tr[s].st;
tr[s].st = numeric_limits<T>::min();
tr[s].add = 0;
}
else if (tr[s].add) {
tr[ls].add += tr[s].add;
tr[rs].add += tr[s].add;
tr[ls].sum += tr[s].add * (tr[ls].r - tr[ls].l + 1);
tr[rs].sum += tr[s].add * (tr[rs].r - tr[rs].l + 1);
tr[ls].mx += tr[s].add;
tr[rs].mx += tr[s].add;
tr[ls].mn += tr[s].add;
tr[rs].mn += tr[s].add;
tr[s].add = 0;
}
}
void build(int l, int r, int s = 1) {
tr[s].l = l, tr[s].r = r;
tr[s].add = T(), tr[s].st = numeric_limits<T>::min();
tr[s].sum = T(), tr[s].mx = numeric_limits<T>::min(), tr[s].mn = numeric_limits<T>::max();
if (l == r) {
tr[s].sum = tr[s].mx = tr[s].mn = dep[dfn[l - 1]];
return;
}
int mid = l + r >> 1;
if (l <= mid) build(l, mid, ls);
if (mid < r) build(mid + 1, r, rs);
pushup(s);
}
void update(int l, int r, T val, int s = 1) {
if (l <= tr[s].l && tr[s].r <= r) {
tr[s].add += val;
tr[s].sum += val * (tr[s].r - tr[s].l + 1);
tr[s].mx += val;
tr[s].mn += val;
return;
}
pushdown(s);
int mid = tr[s].l + tr[s].r >> 1;
if (l <= mid) update(l, r, val, ls);
if (mid < r) update(l, r, val, rs);
pushup(s);
}
void modify(int l, int r, T val, int s = 1) {
if (l <= tr[s].l && tr[s].r <= r) {
tr[s].add = T();
tr[s].st = val;
tr[s].sum = val * (tr[s].r - tr[s].l + 1);
tr[s].mx = tr[s].mn = val;
return;
}
pushdown(s);
int mid = tr[s].l + tr[s].r >> 1;
if (l <= mid) modify(l, r, val, ls);
if (mid < r) modify(l, r, val, rs);
pushup(s);
}
T query(int l, int r, int s = 1) {
if (l <= tr[s].l && tr[s].r <= r) return tr[s].sum;
int mid = tr[s].l + tr[s].r >> 1;
T ans = T();
pushdown(s);
if (l <= mid) ans += query(l, r, ls);
if (mid < r) ans += query(l, r, rs);
return ans;
}
T queryMax(int l, int r, int s = 1) {
if (l <= tr[s].l && tr[s].r <= r) return tr[s].mx;
int mid = tr[s].l + tr[s].r >> 1;
T ans = numeric_limits<T>::min();
pushdown(s);
if (l <= mid) ans = max(ans, queryMax(l, r, ls));
if (mid < r) ans = max(ans, queryMax(l, r, rs));
return ans;
}
T queryMin(int l, int r, int s = 1) {
if (l <= tr[s].l && tr[s].r <= r) return tr[s].mn;
int mid = tr[s].l + tr[s].r >> 1;
T ans = numeric_limits<T>::max();
pushdown(s);
if (l <= mid) ans = min(ans, queryMin(l, r, ls));
if (mid < r) ans = min(ans, queryMin(l, r, rs));
return ans;
}
};
SegmentTree<int> T;
void work() {
cin >> n >> m;
for (int i = 2; i <= n; i++) {
cin >> fa[i];
add(fa[i], i);
}
cinarr(d, n);
dfs(1);
T.build(1, n);
while (m--) {
int op, u;
cin >> op >> u;
if (op == 1) {
int tmp = head[u];
head[u] = tail[u] = sz[u] = 0;
for (int i = tmp; i; i = nxt[i]) {
int v = to[i];
d[u] += d[v];
merge(u, v);
}
cout << sz[u] << ' ' << d[u] << endl;
if (l[u] + 1 <= r[u]) T.update(l[u] + 1, r[u], -1);
}
else cout << T.query(l[u], l[u]) + 1 << endl;
}
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\admin\\CLionProjects\\Practice\\data.in", "r", stdin);
freopen("C:\\Users\\admin\\CLionProjects\\Practice\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int Case = 1;
//cin >> Case;
while (Case--) work();
return 0;
}
/*
_____ _ _ _ __ __
| _ \ | | | | | | \ \ / /
| |_| | | | | | | | \ \/ /
| ___/ | | | | _ | | } {
| | | |_| | | |_| | / /\ \
|_| \_____/ \_____/ /_/ \_\
*/
关于代码的亿点点说明:
- 代码的主体部分位于
void work()
函数中,另外会有部分变量申明、结构体定义、函数定义在上方。#pragma ...
是用来开启 O2、O3 等优化加快代码速度。- 中间一大堆
#define ...
是我习惯上的一些宏定义,用来加快代码编写的速度。"debug.h"
头文件是我用于调试输出的代码,没有这个头文件也可以正常运行(前提是没定义DEBUG
宏),在程序中如果看到dbg(...)
是我中途调试的输出的语句,可能没删干净,但是没有提交上去没有任何影响。ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
这三句话是用于解除流同步,加快输入cin
输出cout
速度(这个输入输出流的速度很慢)。在小数据量无所谓,但是在比较大的读入时建议加这句话,避免读入输出超时。如果记不下来可以换用scanf
和printf
,但使用了这句话后,cin
和scanf
、cout
和printf
不能混用。- 将
main
函数和work
函数分开写纯属个人习惯,主要是为了多组数据。