atcoder abc 453 题解
A - Trimo
问题描述
给定一个长度为 NNN 的字符串 SSS。
输出从 SSS 中删除所有开头连续 o 后得到的字符串。
如果 SSS 中的所有字符都是 o,则输出空字符串。
解题思路
直接从前往后枚举整个字符串,当出现非 o 的字符之后,我们标记一下就开始输出,具体实现看代码。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n, f = 1; // n: 字符串长度, f: 标志位,初始为1表示尚未遇到非'o'字符
string s;
cin >> n >> s; // 读入长度和字符串
for (int i = 0; i < n; i++)
{
// 如果当前字符不是'o',则将标志位设为0,表示已经过了前导'o'的阶段
if (s[i] != 'o')
f = 0;
// 只有当标志位为0时(即已经遇到第一个非'o'字符后),才输出当前字符
// 这样就跳过了开头的所有连续'o'
if (!f)
cout << s[i];
}
return 0;
}
B - Sensor Data Logging
问题描述
在某次测量中,时间 0,1,...,T0,1,\dots,T0,1,...,T 的传感器读数按照以下规则记录:
- 在时间 000,读数被保存。
- 在时间 1,2,...,T1,2,\dots,T1,2,...,T,当且仅当当前读数与最近一次保存的读数之差的绝对值至少为 XXX 时,才保存当前读数。
时间 i=0,1,...,Ti=0,1,\dots,Ti=0,1,...,T 的传感器读数为 AiA_iAi。
请按时间升序输出保存读数的时刻以及保存的数值。
解题思路
我们用一个数组维护上一次保存的读数,接下来按照时间顺序从前往后枚举所有的读数,符合条件我们就输出,顺道维护上一次的读数。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
int t, x; // t: 最大时间, x: 保存读数的差值阈值
cin >> t >> x;
int last; // 最近一次保存的读数
cin >> last; // 读入时间0的读数(一定会被保存)
cout << 0 << ' ' << last << '\n'; // 输出时间0的保存记录
for (int i = 1; i <= t; i++) // 遍历时间1到t
{
int a;
cin >> a; // 读入当前时间i的读数
// 如果当前读数与最近保存的读数之差的绝对值 >= x
if (abs(a - last) >= x)
{
last = a; // 更新最近保存的读数为当前读数
cout << i << ' ' << a << '\n'; // 输出当前时间和读数
}
}
return 0;
}
C - Sneaking Glances
问题描述
高桥在数轴上的坐标 0.50.50.5 处。
他将进行 NNN 次移动。
在第 iii 次移动中,他选择正方向或负方向,并向该方向移动 LiL_iLi。
请问他最多能经过坐标 000 多少次?
在本问题的约束下,没有一次移动会在坐标 000 处结束。
解题思路
因为本题中的 N 非常的小,我们直接 0/1 枚举所有的可能性,然后输出最大的答案即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 25;
int n, l[maxn], ans; // n: 移动次数, l[i]: 第i次移动的距离, ans: 最多经过0的次数
// dfs(当前处理第x次移动, 当前位置pos, 已经经过0的次数t)
void dfs(int x, double pos, int t)
{
if (x == n + 1) // 所有移动都处理完毕
{
ans = max(ans, t); // 更新最大经过次数
return;
}
double p1 = pos - l[x]; // 向负方向移动后的位置
double p2 = pos + l[x]; // 向正方向移动后的位置
// 处理向负方向移动的情况
// 判断是否经过0:移动前和移动后位于0的两侧(一正一负)
if ((pos > 0 && p1 < 0) || (pos < 0 && p1 > 0))
dfs(x + 1, p1, t + 1); // 经过0,次数+1
else
dfs(x + 1, p1, t); // 未经过0,次数不变
// 处理向正方向移动的情况
if ((pos > 0 && p2 < 0) || (pos < 0 && p2 > 0))
dfs(x + 1, p2, t + 1); // 经过0,次数+1
else
dfs(x + 1, p2, t); // 未经过0,次数不变
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> l[i];
dfs(1, 0.5, 0); // 从第1次移动开始,初始位置0.5,经过次数0
cout << ans;
return 0;
}
D - Go Straight
问题描述
有一个 HHH 行 WWW 列的网格,高桥可以在网格中上下左右移动。
从上数第 iii 行、从左数第 jjj 列的格子状态由字符 Si,jS_{i,j}Si,j 表示。
Si,jS_{i,j}Si,j 是 #、.、o、x、S、G 中的一个。
- 若 Si,j=S_{i,j}=Si,j=
#:该格子不可进入。 - 若 Si,j=S_{i,j}=Si,j=
.:该格子可以自由进出。即进入该格子后,高桥可以向任意相邻格子(如果存在)上下左右移动。 - 若 Si,j=S_{i,j}=Si,j=
o:在该格子中,高桥必须沿与上一步移动相同的方向移动。即进入该格子后,他必须不改变方向地移动到下一个格子。 - 若 Si,j=S_{i,j}=Si,j=
x:在该格子中,高桥不能沿与上一步移动相同的方向移动。即进入该格子后,他必须改变方向才能移动到下一个格子。180 度掉头返回前一个格子也算改变方向。 - 若 Si,j=S_{i,j}=Si,j=
S:该格子是起点的格子。该格子可以自由进出。 - 若 Si,j=S_{i,j}=Si,j=
G:该格子是终点的格子。该格子可以自由进出。
恰有一个 (i,j)(i,j)(i,j) 满足 Si,j=S_{i,j}=Si,j=S,也恰有一个满足 Si,j=S_{i,j}=Si,j=G。
高桥希望从起点出发,通过反复向上下左右相邻格子移动到达终点。
判断是否可能,如果可能,输出一个合法的移动序列,移动次数不超过 5×1065 \times 10^65×106 次。
可以证明,在问题条件下,如果存在合法的移动序列,则一定存在一个移动次数不超过 5×1065 \times 10^65×106 的序列。
只要移动次数不超过 5×1065 \times 10^65×106,不需要最小化移动次数。
解题思路
记忆化搜索,我们用 vis[x][y][d]vis[x][y][d]vis[x][y][d] 来表示从某一个方向到达 (x,y)(x,y)(x,y) 的情况是否出现过。接下来我们直接 bfs 即可,如果能够到达终点,我们就通过 visvisvis 数组 + dfsdfsdfs 来实现逆向的还原,
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1005;
int h, w;
// vis[x][y][dir] 记录在格子(x,y)且上一步移动方向为dir的状态是否已被访问过
// 方向: 0右(R),1左(L),2下(D),3上(U)
int vis[maxn][maxn][4];
int X[4] = {0, 0, 1, -1}; // 方向对应的行变化
int Y[4] = {1, -1, 0, 0}; // 方向对应的列变化
string s[maxn]; // 存储网格,索引从1开始
string D = "RLDU"; // 方向字符,与X,Y索引对应
int sx, sy, ex, ey; // 起点(S)和终点(G)坐标
// 递归回溯输出路径(从终点反向回溯到起点)
// 参数: 当前格子坐标(x,y), 进入当前格子时的方向dir(即从上一个格子移动过来的方向)
bool getans(int x, int y, int dir)
{
// 计算上一个格子坐标(即当前移动的反方向)
int nx = x - X[dir], ny = y - Y[dir];
// 如果上一个格子是起点,说明找到了完整路径
if (nx == sx && ny == sy)
{
cout << D[dir]; // 输出最后一步的方向
return true;
}
// 遍历所有可能的上一步方向
for (int i = 0; i < 4; i++)
{
// 规则约束:如果上一个格子是 'o',则上一步的方向必须与进入当前格子的方向相同(即dir == i)
if (s[nx][ny] == 'o' && dir != i)
continue;
// 规则约束:如果上一个格子是 'x',则上一步的方向不能与进入当前格子的方向相同(即dir != i)
if (s[nx][ny] == 'x' && dir == i)
continue;
// 如果该状态之前被访问过,则尝试回溯
if (vis[nx][ny][i])
{
vis[nx][ny][i] = 0; // 标记已用于输出,避免重复使用
bool f = getans(nx, ny, i);
if (f)
{
cout << D[dir]; // 输出当前这一步的方向
return true;
}
}
}
return false;
}
void solve()
{
// BFS队列: (行, 列, 进入该格子时的方向)
queue<tuple<int, int, int>> q;
// 初始化:从起点出发,四个方向都可以作为"初始方向"
// 注意起点没有"上一步",这里把起点视为可以从任意方向进入
for (int i = 0; i < 4; i++)
{
vis[sx][sy][i] = 1;
q.push({sx, sy, i});
}
while (!q.empty())
{
auto [x, y, d] = q.front();
q.pop();
// 到达终点
if (x == ex && y == ey)
{
cout << "Yes\n";
// 从终点反向输出路径
getans(x, y, d);
return;
}
// 尝试向四个方向移动
for (int i = 0; i < 4; i++)
{
int nx = x + X[i], ny = y + Y[i]; // 下一个格子坐标
// 边界检查、未访问、不是墙
if (nx >= 1 && nx <= h && ny >= 1 && ny <= w && !vis[nx][ny][i] && s[nx][ny] != '#')
{
// 规则约束:当前格子是 'o' 时,移动方向必须与进入当前格子的方向相同
if (s[x][y] == 'o' && i != d)
continue;
// 规则约束:当前格子是 'x' 时,移动方向不能与进入当前格子的方向相同
if (s[x][y] == 'x' && i == d)
continue;
// 注意:'.'、'S'、'G' 没有额外约束,可以任意方向移动
q.push({nx, ny, i});
vis[nx][ny][i] = 1;
}
}
}
// BFS结束未找到终点
cout << "No";
return;
}
int main()
{
cin >> h >> w;
// 读入网格,每行前加一个空格,使下标从1开始方便处理
for (int i = 1; i <= h; i++)
cin >> s[i], s[i] = ' ' + s[i];
// 寻找起点和终点坐标
for (int i = 1; i <= h; i++)
for (int j = 1; j <= w; j++)
if (s[i][j] == 'S')
sx = i, sy = j;
else if (s[i][j] == 'G')
ex = i, ey = j;
solve();
return 0;
}
E - Team Division
问题描述
将 NNN 名玩家(玩家 111,玩家 222,...,玩家 NNN)分成两个(可区分的)队伍 AAA 和 BBB,满足以下所有条件:
- 每个队伍至少包含一名玩家。
- 每名玩家要么属于 AAA 队,要么属于 BBB 队,但不能同时属于两队。
- 玩家 iii 所属队伍的人数至少为 LiL_iLi,至多为 RiR_iRi。
求满足条件的分配队伍的方式数,并将结果对 998244353998244353998244353 取模输出。
如果存在某个玩家在两个分配方案中所属队伍不同,则视为不同的分配方案。
解题思路
我们可以枚举 a 队的人数,进而求解 b 的人数和符合条件的情况。首先我们需要排除一些情况,当我们枚举出 a 队和 b 队的人数的时候,如果一个人即无法去 a 队,也无法去 b 队,那么这个方案就是不合法的。
这里举个例子,假设 N = 10,那么此时假设 a 队的人数小于 b 队,人数为 num,某个人的人数区间为 [2, 4],那么此时 num 就不能小于 2,并且不能超过 4。
接下来我们用差分维护对于一个具体的人数,可以选择哪些人,假设现在可以去 a 队的人数是 A,可以去 b 队的人数是 B,那么此时既可以去 a 又可以去 b 的人数就是 C = A + B - N。
剩下来的部分用排列组合求解即可,有 A - C 个人只能去 a 队,假设 a 队还需要 m 个人,就可以从 C 里面选,最终结果就是 CCmC_C^mCCm,直接费马小定理求解。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 2e5 + 5, MOD = 998244353;
int n, l[maxn], r[maxn], d[maxn], m[maxn], L, R;
// 快速幂,用于计算逆元
int quick_pow(int a, int b)
{
int res = 1;
while (b)
{
if (b & 1)
res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
// 组合数 C(a, b) = a! / (b! * (a-b)!)
int C(int a, int b)
{
return m[a] * quick_pow(m[b], MOD - 2) % MOD * quick_pow(m[a - b], MOD - 2) % MOD;
}
signed main()
{
cin >> n;
// 预处理阶乘 m[i] = i!
m[0] = m[1] = 1;
for (int i = 2; i <= n; i++)
m[i] = m[i - 1] * i % MOD;
// 队伍A的人数可能范围是 [1, n-1](两队都至少1人)
// 又因为队伍A和队伍B人数互换视为不同(队伍可区分),所以只需枚举A的人数i
// 但这里L,R是"可能合法的A的人数范围"的初始估计
// 初始取 L = 1, R = (n+1)/2 只是为了缩小范围?实际上后续会调整
L = 1, R = (n + 1) / 2;
// 读入每个玩家的限制 [l_i, r_i]
for (int i = 1; i <= n; i++)
{
cin >> l[i] >> r[i];
// 调整 L 和 R:排除那些对于所有玩家 i,i 和 n-i 都不在 [l_i, r_i] 内的情况
// 具体解释见下方整体思路
while (L <= R && (L < l[i] || L > r[i]) && (n - L < l[i] || n - L > r[i]))
L++;
while (L <= R && (R < l[i] || R > r[i]) && (n - R < l[i] || n - R > r[i]))
R--;
// 差分数组 d:统计对于每个可能的人数 k,有多少玩家要求人数≥k(即允许人数包含k)
// 这里 d[l[i]]++ 表示从 l[i] 开始的人数都"满足"该玩家(即人数≥l[i])
// d[r[i]+1]-- 表示人数超过 r[i] 后不再满足
d[l[i]]++, d[r[i] + 1]--;
}
// 对差分数组做前缀和,得到 d[k] = 允许队伍人数为 k 的玩家数量
for (int i = 1; i <= n; i++)
d[i] += d[i - 1];
int ans = 0;
// 枚举队伍A的人数 i(1 ≤ i ≤ n-1),则队伍B的人数为 j = n-i
for (int i = 1; i < n; i++)
{
int j = n - i;
int min_num = min(i, j); // 用于快速检查,实际上后面会用到 L,R 约束
// 如果 min_num 不在当前确定的 [L,R] 范围内,跳过
if (min_num < L || min_num > R)
continue;
// d[i] = 允许队伍人数为 i 的玩家数量(即这些玩家可以放在队伍A)
// d[j] = 允许队伍人数为 j 的玩家数量(即这些玩家可以放在队伍B)
// 但注意:一个玩家如果同时允许 i 和 j,则它可以属于两队中的任意一队
// 用容斥原理:设 c = 同时允许 i 和 j 的玩家数(即"自由玩家")
// 则:d[i] = 仅允许i + 同时允许
// d[j] = 仅允许j + 同时允许
// 所以:c = d[i] + d[j] - n
int a = d[i], b = d[j];
int c = a + b - n; // 同时允许 i 和 j 的玩家数
a -= c; // 仅允许队伍人数为 i(即只能去A)的玩家数
b -= c; // 仅允许队伍人数为 j(即只能去B)的玩家数
// 检查合理性:仅允许去A的人数不能超过A的实际人数 i
// 同样仅允许去B的人数不能超过B的实际人数 j
if (a > i || b > j)
continue;
// 在同时允许 i 和 j 的 c 个玩家中,需要选择 (i - a) 个放入队伍A
// 剩下的 c - (i - a) = j - b 个自然放入队伍B
// 因此方案数为 C(c, i - a)
ans += C(c, i - a);
ans %= MOD;
}
cout << ans;
return 0;
}
F - Avoid Division
问题描述
给定一棵有 NNN 个顶点的树。
顶点编号为顶点 111,顶点 222,...,顶点 NNN,第 iii 条边(1≤i≤N−11 \leq i \leq N-11≤i≤N−1)连接顶点 UiU_iUi 和 ViV_iVi。
高桥将用颜色 1,2,...,K1, 2, \ldots, K1,2,...,K 中的颜色为每个顶点染色。
颜色 iii 最多可以用于染 CiC_iCi 个顶点。
判断是否存在满足以下条件的染色方案,如果存在,则输出一种合法的染色方案。
- 对于每条边,都存在某个 iii(1≤i≤K1 \leq i \leq K1≤i≤K),使得沿着该边切断树后得到的两个子树中,每个子树都至少包含一个颜色为 iii 的顶点。
给定 TTT 个测试用例,请对每个用例求解。
解题思路
首先我们需要知道,如果两个点的颜色相同,那么就相当于给这两个点连了一条边,我们最终的目标是让整个图上不存在任何的桥。
首先我们要做的第一件事情是找到一个点,这个点有点类似于重心。假设整棵树一共有 m 个节点,那我们找到的这个点的任意一棵子树中叶子节点的数量不能超过 m / 2,然后根据 dfs 序,我们把所有的叶子节点排列起来。

例如上图,我们直接把所有的叶子节点排列起来,就有了 [1, 2, 3, 4, 5](其他情况也是相同的),然后我们把他们分成两部分,分别是 [1, 2, 3] 和 [4, 5],这里左半边上取整是因为我们要让匹配的两点相距尽可能远。
接下来,1 和 4 匹配,2 和 5 匹配,3 没有可以匹配的点了就和重心去匹配,让他们变成相同的颜色。
这样的话,连起来的点一定都会经过重心,不会出现有桥出现的情况。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000010;
int n, k;
int a[maxn];
pair<int, int> color_limit[maxn]; // first: 容量C_i, second: 颜色编号
int head[maxn], nxt[maxn], to[maxn], degree[maxn], edge_idx;
// 添加无向边
void add_edge(int x, int y)
{
++edge_idx;
to[edge_idx] = y;
nxt[edge_idx] = head[x];
head[x] = edge_idx;
}
int subtree_leaf_cnt[maxn]; // 子树中叶子节点的个数
int total_leaves, center; // 总叶子数,树的重心(按叶子数定义)
int color_idx, node_order[maxn], answer[maxn]; // node_order: 节点的染色顺序,answer: 最终颜色
// 第一次DFS:计算以每个节点为根的子树中的叶子数,并找到"叶子重心"
void dfs_calc(int u, int fa)
{
subtree_leaf_cnt[u] = (degree[u] == 1 ? 1 : 0); // 叶子节点自身算1
int max_sub = 0;
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (v != fa)
{
dfs_calc(v, u);
max_sub = max(max_sub, subtree_leaf_cnt[v]);
subtree_leaf_cnt[u] += subtree_leaf_cnt[v];
}
}
// 考虑去掉u后,剩余部分(父方向)的叶子数
max_sub = max(max_sub, total_leaves - subtree_leaf_cnt[u]);
// 如果最大子树的叶子数 ≤ 总叶子数/2,则u可作为"叶子重心"
if (max_sub <= total_leaves / 2)
center = u;
}
// 第二次DFS:收集以center为根的子树中的叶子节点(按顺序)
void dfs_collect(int u, int fa)
{
if (degree[u] == 1) // 叶子节点
node_order[++color_idx] = u;
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (v != fa)
dfs_collect(v, u);
}
}
void solve()
{
total_leaves = center = color_idx = edge_idx = 0;
cin >> n >> k;
for (int i = 1; i <= n; i++)
degree[i] = head[i] = 0;
// 读入树边
for (int i = 1; i <= n - 1; i++)
{
int x, y;
cin >> x >> y;
add_edge(x, y);
add_edge(y, x);
degree[x]++;
degree[y]++;
}
// 统计叶子节点数
for (int i = 1; i <= n; i++)
if (degree[i] == 1)
total_leaves++;
// 读入每种颜色的容量限制
int sum_usable = 0;
for (int i = 1; i <= k; i++)
{
cin >> color_limit[i].first;
color_limit[i].second = i;
if (color_limit[i].first > 1)
sum_usable += color_limit[i].first;
sum_usable = min(sum_usable, total_leaves);
}
// 如果不能覆盖所有叶子节点,则无解
if (sum_usable < total_leaves)
{
cout << -1 << "\n";
return;
}
// 找到"叶子重心"
dfs_calc(1, 0);
// 先收集重心下的叶子
dfs_collect(center, 0);
// 如果重心不是叶子,则重心本身也需要染色(放在最后处理)
if (degree[center] != 1)
node_order[++color_idx] = center;
// 收集所有非叶子、非重心的节点
for (int i = 1; i <= n; i++)
if (degree[i] != 1 && i != center)
node_order[++color_idx] = i;
// 双指针分配颜色:左指针指向叶子序列前半,右指针指向叶子序列后半
int left = 1, right = left + (total_leaves + 1) / 2;
int cnt = 0; // 已染色的节点数
// 先处理容量 > 1 的颜色(用于叶子节点的交替染色)
for (int i = 1; i <= k; i++)
{
if (color_limit[i].first == 1)
continue;
while (color_limit[i].first)
{
++cnt;
if (cnt <= total_leaves)
{
// 交替放到左右两侧,保证每条边两侧都有该颜色
if (cnt & 1)
answer[node_order[left++]] = color_limit[i].second;
else
answer[node_order[right++]] = color_limit[i].second;
}
else
{
// 超过叶子数后,剩余的节点(重心、内部节点)随意放
answer[node_order[right++]] = color_limit[i].second;
}
if (cnt == n)
{
for (int j = 1; j <= n; j++)
cout << answer[j] << " ";
cout << "\n";
return;
}
color_limit[i].first--;
}
}
// 再处理容量 = 1 的颜色(只能染一个节点,通常放在叶子或内部)
for (int i = 1; i <= k; i++)
{
while (color_limit[i].first)
{
++cnt;
if (cnt <= total_leaves)
{
if (cnt & 1)
answer[node_order[left++]] = color_limit[i].second;
else
answer[node_order[right++]] = color_limit[i].second;
}
else
{
answer[node_order[right++]] = color_limit[i].second;
}
if (cnt == n)
{
for (int j = 1; j <= n; j++)
cout << answer[j] << " ";
cout << "\n";
return;
}
color_limit[i].first--;
}
}
}
int main()
{
int t;
cin >> t;
while (t--)
solve();
return 0;
}
G - Copy Query
问题描述
有 NNN 个长度为 MMM 的序列:A1,A2,...,ANA_1, A_2, \dots, A_NA1,A2,...,AN。初始时,所有序列的所有元素均为 000。
以下,序列 AiA_iAi 的第 jjj 个元素记为 Ai,jA_{i,j}Ai,j。
按给定顺序处理以下三种类型的查询,总共 QQQ 个查询。
- 类型 1:将序列 AXiA_{X_i}AXi 覆盖为序列 AYiA_{Y_i}AYi。即,对每个整数 jjj(1≤j≤M1 \le j \le M1≤j≤M),将 AXi,jA_{X_i,j}AXi,j 改为 AYi,jA_{Y_i,j}AYi,j。
- 类型 2:将序列 AXiA_{X_i}AXi 的第 YiY_iYi 个元素 AXi,YiA_{X_i,Y_i}AXi,Yi 改为 ZiZ_iZi。
- 类型 3:对于序列 AXiA_{X_i}AXi,输出从第 LiL_iLi 个到第 RiR_iRi 个元素的和,即 AXi,Li+AXi,Li+1+⋯+AXi,RiA_{X_i,L_i} + A_{X_i,L_i+1} + \dots + A_{X_i,R_i}AXi,Li+AXi,Li+1+⋯+AXi,Ri。
解题思路
主席树模板题。
由于类型 1 操作要求将一个序列整体覆盖为另一个序列,如果直接复制整个数组(长度 M 可达 2e5),在最坏情况下会超时。
这里利用可持久化线段树的特性:每个序列对应一个"根节点",根节点指向一棵线段树,该线段树保存了这个序列所有位置的值。
因为线段树可以共享未修改的节点,所以复制一个序列只需让目标根指针指向源根节点,O(1) 完成。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 线段树节点结构
struct Node
{
int left, right; // 左右子节点的索引
ll sum; // 当前节点表示的区间和
Node() : left(0), right(0), sum(0) {}
};
const int maxq = 200010; // 最大操作数
const int maxm = 200010; // 序列最大长度
vector<Node> tree; // 存储所有线段树节点(可持久化)
int roots[maxq]; // 每个序列对应的根节点索引
int version_count = 0; // 版本计数(本题未直接使用,但可辅助调试)
// 构建一棵空线段树(所有位置为0),返回根节点索引
int build(int l, int r)
{
int idx = tree.size(); // 新节点的索引
tree.emplace_back(); // 添加一个空节点
if (l == r)
{
tree[idx].sum = 0; // 叶子节点,值为0
return idx;
}
int mid = (l + r) >> 1;
// 递归构建左右子树
tree[idx].left = build(l, mid);
tree[idx].right = build(mid + 1, r);
// 内部节点:左右子树的和
tree[idx].sum = tree[tree[idx].left].sum + tree[tree[idx].right].sum;
return idx;
}
// 单点更新:将位置 pos 的值设为 val
// prev_root: 上一个版本的根节点,l,r: 当前区间,返回新版本的根节点索引
int update(int prev_root, int l, int r, int pos, ll val)
{
int idx = tree.size(); // 新节点索引
tree.emplace_back(); // 创建新节点
tree[idx] = tree[prev_root]; // 先复制旧节点的左右孩子和sum(浅拷贝)
if (l == r)
{ // 到达叶子节点
tree[idx].sum = val; // 直接修改值
return idx;
}
int mid = (l + r) >> 1;
if (pos <= mid)
{
// 修改左子树,右子树复用旧版本
tree[idx].left = update(tree[prev_root].left, l, mid, pos, val);
}
else
{
// 修改右子树,左子树复用旧版本
tree[idx].right = update(tree[prev_root].right, mid + 1, r, pos, val);
}
// 更新当前节点的sum
tree[idx].sum = tree[tree[idx].left].sum + tree[tree[idx].right].sum;
return idx;
}
// 区间查询:返回区间 [ql, qr] 的和
ll query(int node, int l, int r, int ql, int qr)
{
if (ql <= l && r <= qr)
{
return tree[node].sum; // 当前区间完全被包含,直接返回
}
int mid = (l + r) >> 1;
ll res = 0;
if (ql <= mid)
{
res += query(tree[node].left, l, mid, ql, qr);
}
if (qr > mid)
{
res += query(tree[node].right, mid + 1, r, ql, qr);
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M, Q;
cin >> N >> M >> Q;
// 预分配空间,避免频繁扩容
tree.reserve(maxq * 20);
// 构建一棵空线段树(所有位置为0)
int empty_root = build(1, M);
// 初始化:N个序列都指向这棵空树
for (int i = 1; i <= N; i++)
{
roots[i] = empty_root;
}
version_count = 1; // 记录版本数(这里仅作示意,实际未用于逻辑)
while (Q--)
{
int t;
cin >> t;
if (t == 1)
{
// 类型1:将序列 X 覆盖为序列 Y(指针赋值,O(1))
int X, Y;
cin >> X >> Y;
roots[X] = roots[Y]; // 直接让X指向Y的根节点
}
else if (t == 2)
{
// 类型2:单点修改,产生新版本
int X, Y, Z;
cin >> X >> Y >> Z;
roots[X] = update(roots[X], 1, M, Y, Z); // 更新后得到新根
}
else if (t == 3)
{
// 类型3:区间求和查询
int X, L, R;
cin >> X >> L >> R;
ll ans = query(roots[X], 1, M, L, R);
cout << ans << '\n';
}
}
return 0;
}