atcoder ABC 453 题解

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 是 #.oxSG 中的一个。

  • 若 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;
}
相关推荐
小则又沐风a2 小时前
STL库: string类
开发语言·c++
田梓燊2 小时前
leetcode 48
算法·leetcode·职场和发展
mmz12072 小时前
深度优先搜索DFS2(c++)
c++·算法·深度优先
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 169. 多数元素 | C++ 哈希表基础解法
c++·leetcode·散列表
米粒12 小时前
力扣算法刷题 Day 38 (打家劫舍专题)
算法·leetcode·职场和发展
暴力求解2 小时前
C++ ---string类(三)
开发语言·c++
琪伦的工具库2 小时前
批量PDF合并工具使用说明:批量合并与直接合并两种模式,拖拽排序/页面范围/遍历子目录/重名自动处理
数据结构·pdf·排序算法
Robot_Nav2 小时前
RC-ESDF与Lazy Theta* 算法结合进行离线全局路径的生成
算法·全局规划·esdf
papership2 小时前
【入门级-算法-7、搜索算法:深度优先搜索】
算法·深度优先