AtCoder Beginner Contest 465

2026年7月4日 100 分钟 $7$ 题 · A ~ G

标签分析 · AtCoder Beginner Contest 465

AtCoder ABC465 $7$ 题 · A ~ G

整体标签汇总

难度分布

难度题数对应题目
入门$2$A, B
普及$1$C
普及+$1$D
提高$1$E
提高+$1$F
省选$1$G

算法分布

算法大类题数题目
数学$2$A, B
模拟$2$B, C
树 / LCA$1$D
数位DP$1$E
高维前缀和 / 容斥原理$1$F
类欧几里得算法 / 数论$1$G

数据结构分布

数据结构题数题目
双端队列$1$C
map区间维护$1$G

技巧分布

技巧题数题目
区间交集$1$B
懒标记$1$C
K叉树 / 最短路径$1$D
状态压缩$2$E, F
滚动数组$1$E
SOS DP$1$F
离线处理$1$G

易错点分布

易错类型题数
溢出(long long / __int128)$3$
边界 / 越界$2$
特殊值 / 特判$3$
前导零 / 浮点精度$2$

各题标签详情

A. Supermajority

难度 入门 AtCoder
题型 数学
算法 数学
数据结构
技巧
易错点 浮点精度 严格大于
入门 | AtCoder | 数学 | 数学 | - | - | 浮点精度 | 严格大于

B. Parking 2

难度 入门 AtCoder
题型 模拟
算法 数学
数据结构
技巧 区间交集
易错点 边界
入门 | AtCoder | 模拟 | 数学 | - | 区间交集 | 边界

C. Reverse Permutation

难度 普及 AtCoder
题型 模拟
算法
数据结构 双端队列
技巧 懒标记
易错点 最终输出方向
普及 | AtCoder | 模拟 | - | 双端队列 | 懒标记 | 最终输出方向

D. X to Y

难度 普及+ AtCoder
题型 数学
算法 树 / LCA
数据结构
技巧 K叉树 最短路径
易错点 $X=0$ 或 $Y=0$ 的路径构造 long long 溢出
普及+ | AtCoder | 数学 | 树 | LCA | - | K叉树 | 最短路径 | long long溢出

E. Digit Circus

难度 提高 AtCoder
题型 数学
算法 数位DP
数据结构
技巧 状态压缩 滚动数组 取模
易错点 前导零处理 $x=0$ 排除
提高 | AtCoder | 数学 | 数位DP | - | 状态压缩 | 滚动数组 | 取模 | 前导零处理

F. Sjeltzer?

难度 提高+ AtCoder
题型 前缀和
算法 高维前缀和 / 容斥原理
数据结构
技巧 状态压缩 SOS DP
易错点 越界($x_k-1 \lt 0$) long long 溢出 $x_k \gt y_k$ 特判
提高+ | AtCoder | 前缀和 | 高维前缀和 | SOS DP | - | 容斥原理 | 状态压缩 | long long溢出

G. Sum of Mex of Mod of Linear

难度 省选 AtCoder
题型 数学
算法 类欧几里得算法 / 数论
数据结构 map区间维护
技巧 离线处理 环上问题转化 区间维护
易错点 环状连通特判 满环特判 __int128 溢出
省选 | AtCoder | 数学 | 类欧几里得算法 | 数论 | map区间维护 | 离线处理 | 环上问题转化 | 满环特判

A. Supermajority

入门 AtCoder 数学 不等式 浮点精度 时间限制:$2$ s 空间限制:$1024$ MB

$1$. 题目大意

给定两个正整数 $A$ 和 $B$($1 \le A, B \le 1000$),判断 $A$ 是否严格大于 $B$ 的三分之二,即判断 $A > B \times \frac{2}{3}$ 是否成立。若成立则输出 Yes,否则输出 No

$2$. 算法分析

$2.1$ 直观做法:浮点数运算的隐患

最直接的做法是计算 A > B * 2.0 / 3.0。虽然在本题数据范围($1 \le A, B \le 1000$)下双精度浮点数尚不至于产生致命误差,但直接比较浮点数大小在信息学竞赛中是一个极易踩坑的操作。其原因在于,计算机以二进制存储实数,许多十进制小数(如 $\frac{2}{3} = 0.666\dots$)无法精确表示,存储结果是一个近似值。浮点运算的舍入误差可能导致诸如 $0.999999999999999$ 与 $1.0$ 比较时得到错误结论,从而引发 WA(Wrong Answer)。

$2.2$ 满分做法:整数等价变形

为了彻底规避浮点数精度问题,应将公式转化为纯整数运算。在原不等式 $$A > B \times \frac{2}{3}$$ 两边同时乘以正数 $3$(不等号方向不变),可得完全等价的整数形式: $$3 \times A > 2 \times B$$ 通过这一简单的数学变形,除法和浮点乘法被替换为纯粹的整数乘法和比较。整数运算不仅速度更快,而且结果绝对精确。

核心结论:判断 $A > B \times \frac{2}{3}$ 等价于判断 $3A > 2B$,全程只涉及整数运算,零精度风险。

$2.3$ 正确性与边界分析

$3$. 算法思路 & 复杂度

$3.1$ 算法步骤

  1. 从标准输入读入两个整数 $A$ 和 $B$。
  2. 计算 $3A$ 和 $2B$。
  3. 若 $3A > 2B$,输出 Yes;否则输出 No

$3.2$ 复杂度分析

项目复杂度说明
时间复杂度$O(1)$仅涉及常数次乘法和比较运算
空间复杂度$O(1)$仅需存储 $A$ 和 $B$ 两个变量

$4$. 完整代码

#include <iostream>

using namespace std;

int main() {
    // 优化标准输入输出流的性能
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int A, B;
    if (cin >> A >> B) {
        // 核心逻辑:将 A > B * (2/3) 转化为 3 * A > 2 * B
        // 避免浮点数造成的精度丢失问题
        if (3 * A > 2 * B) {
            cout << "Yes\n";
        } else {
            cout << "No\n";
        }
    }

    return 0;
}

$5$. 样例说明

题目提供了 $3$ 组样例,下面逐一演算:

$\#$$A$$B$$3A$$2B$$3A > 2B$?输出
$1$$316$$465$$948$$930$ 是($948 > 930$)Yes
$2$$101$$248$$303$$496$ 否($303 \not> 496$)No
$3$$666$$999$$1998$$1998$ 否($1998 \not> 1998$,相等不成立)No

样例 $3$ 详析:当 $A = 666, B = 999$ 时,$B \times \frac{2}{3} = 999 \times \frac{2}{3} = 666$,恰好等于 $A$。题目要求的是严格大于($>$),因此 $A = B \times \frac{2}{3}$ 时不等式不成立,应输出 No。整数变形后 $3A = 1998$ 与 $2B = 1998$ 相等,$3A > 2B$ 为假,结论一致。

$6$. 注意事项与要点归纳

$6.1$ 浮点精度规避

$6.2$ 严格大于的边界陷阱

$6.3$ 溢出与数据类型

$6.4$ 思维线索回顾

思考路径:读题 $A > B \times \frac{2}{3}$ → 识别浮点精度风险 → 不等式两边同乘 $3$ → 得到整数等价式 $3A > 2B$ → 读入整数直接比较 → 输出结果。整个过程只需一次数学变形和一次整数比较,时间复杂度 $O(1)$。

B. Parking 2

入门 AtCoder 区间交集 数学 模拟 时间限制:$2$ s 空间限制:$1024$ MB

$1$. 题目大意

某停车场分段计费:在 $L$ 时到 $R$ 时的时段内,每小时收费 $X$;在该时段之外的其余小时,每小时收费 $Y$。 一辆车从恰好 $A$ 时停到恰好 $B$ 时(不跨越午夜),求总停车费用。

输入 $6$ 个整数 $X,\; Y,\; L,\; R,\; A,\; B$,满足 $1 \le X, Y \le 1000$,$1 \le L \lt R \le 23$,$1 \le A \lt B \le 23$。输出一个整数表示总费用。

$2$. 算法分析

$2.1$ 解法一:暴力按小时模拟

由于时间范围极小($A, B \le 23$,最多 $22$ 个小时),最直接的方法是模拟停车期间的每一个小时:

此方法清晰直观,时间复杂度 $O(B - A)$,在本题数据范围下可以轻松通过(AC)。但思考:如果停车时长跨度极大(如 $B - A$ 可达 $10^9$),循环模拟就会超时(TLE)。

$2.2$ 解法二:区间交集公式(最优 $O(1)$ 解法)

为了得到不依赖时间跨度的常数时间算法,我们将问题抽象为求两个线段的交集长度:

核心公式(区间交集长度):
设 $start = \max(A, L)$,$end = \min(B, R)$,则两区间的交集长度为: $$Overlap = \max(0,\; end - start)$$ 外层 $\max(0, \cdot)$ 巧妙处理了区间完全不相交(相离)的情形——此时 $end - start \le 0$,不会出现负数时长。

由此可得:

该解法仅涉及 $O(1)$ 次基本运算,与 $B - A$ 的大小完全无关。

$2.3$ 正确性与边界处理

区间交集公式能够覆盖两区间之间所有可能的位置关系:

关于数据溢出:$X, Y \le 1000$,时长最多 $B - A \le 22$,总费用最大约为 $22 \times 1000 = 22000$,完全在 $32$ 位有符号整数 $\texttt{int}$ 的表示范围内,无需担心溢出。

$3$. 算法思路 & 复杂度

$3.1$ 算法流程

  1. 读入 $X,\; Y,\; L,\; R,\; A,\; B$。
  2. 计算总时长 $total = B - A$。
  3. 计算交集起点 $start = \max(A, L)$,终点 $end = \min(B, R)$。
  4. 计算特殊时长 $special = \max(0,\; end - start)$。
  5. 计算普通时长 $normal = total - special$。
  6. 输出答案 $ans = special \times X + normal \times Y$。

$3.2$ 复杂度分析

项目复杂度说明
时间复杂度$O(1)$仅涉及常数的加减法和最值运算,与停车时长无关
空间复杂度$O(1)$仅使用几个整型变量存储输入和中间结果

$4$. 完整代码

#include <iostream>
#include <algorithm>

using namespace std;

int main() {
    // 优化标准输入输出流
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int X, Y, L, R, A, B;
    if (!(cin >> X >> Y >> L >> R >> A >> B)) return 0;

    // 1. 计算总停车时长
    int total_hours = B - A;

    // 2. 计算在特殊收费时段 [L, R] 内的停车时长(即求区间交集)
    int overlap_start = max(A, L);
    int overlap_end = min(B, R);
    
    // NOTE: 使用 max(0, ...) 巧妙处理两个区间完全不相交的情况,防止出现负数时长
    int special_hours = max(0, overlap_end - overlap_start);

    // 3. 计算在普通收费时段内的停车时长
    int normal_hours = total_hours - special_hours;

    // 4. 计算总费用
    int total_fee = special_hours * X + normal_hours * Y;

    cout << total_fee << "\n";

    return 0;
}

$5$. 样例说明

下面逐条演算所有 $3$ 组样例,展示区间交集公式的计算过程:

$\#$$X$$Y$$L$$R$$A$$B$演算过程答案
$1$$700$$300$$9$$17$$7$$21$ $total = 21 - 7 = 14$
$start = \max(7, 9) = 9$
$end = \min(21, 17) = 17$
$special = \max(0, 17 - 9) = 8$
$normal = 14 - 8 = 6$
$ans = 8 \times 700 + 6 \times 300 = 5600 + 1800 = 7400$
$7400$
$2$$600$$500$$9$$17$$17$$20$ $total = 20 - 17 = 3$
$start = \max(17, 9) = 17$
$end = \min(20, 17) = 17$
$special = \max(0, 17 - 17) = 0$
$normal = 3 - 0 = 3$
$ans = 0 \times 600 + 3 \times 500 = 1500$
$1500$
$3$$900$$200$$12$$14$$11$$13$ $total = 13 - 11 = 2$
$start = \max(11, 12) = 12$
$end = \min(13, 14) = 13$
$special = \max(0, 13 - 12) = 1$
$normal = 2 - 1 = 1$
$ans = 1 \times 900 + 1 \times 200 = 1100$
$1100$

样例 $3$ 详析:$X=900,\; Y=200,\; L=12,\; R=14,\; A=11,\; B=13$。停车总时长 $2$ 小时:$11 \to 12$ 时(普通费率 $200$)和 $12 \to 13$ 时(特殊费率 $900$)。区间交集公式:$start = 12$,$end = 13$,$special = 1$,$normal = 1$,总费用 $900 + 200 = 1100$。

$6$. 注意事项与要点归纳

$6.1$ 易错点与边界

$6.2$ 思维线索回顾

思考路径:分段计费 → 总费用 = 特殊费率部分 + 普通费率部分 → 特殊费率时长 = 两个时间区间的交集长度 → $Overlap = \max(0, \min(B, R) - \max(A, L))$ → 代入公式一步到位 → $O(1)$。

$6.3$ 扩展思考


C. Reverse Permutation

普及 AtCoder 双端队列 懒标记 模拟 翻转 时间限制:$2$ s 空间限制:$1024$ MB

$1$. 题目大意

初始序列 $A$ 为空。依次进行 $k = 1, 2, \dots, N$ 的操作: 将 $k$ 追加到序列 $A$ 的末尾,然后查看字符串 $S$ 的第 $k$ 个字符 $S_k$:

求全部 $N$ 步操作完成后序列 $A$ 的最终排列($2 \le N \le 5 \times 10^5$)。

输入:第一行一个整数 $N$,第二行一个由 $\text{'o'}$ 和 $\text{'x'}$ 组成、长度为 $N$ 的字符串 $S$。
输出:一行 $N$ 个整数,空格分隔,表示最终序列。

$2$. 算法分析

$2.1$ 暴力做法 ($O(N^2)$)

最直观的做法是用 std::vector 或数组模拟题目描述的过程: 第 $k$ 步 push_back(k),若 $S_k = \text{'o'}$ 则调用 std::reverse 翻转整个数组。

每次翻转的时间复杂度为 $O(k)$。最坏情况下($S$ 全为 $\text{'o'}$),总时间复杂度为 $\sum_{k=1}^N O(k) = O(N^2)$。对于 $N = 5 \times 10^5$ 的数据范围,这必然导致超时(TLE)。

$2.2$ 正解推导:双端队列 + 懒标记 ($O(N)$)

暴力做法的瓶颈在于每一次 $S_k = \text{'o'}$ 都要进行 $O(k)$ 的物理翻转。 仔细观察操作特性:每次只在当前序列的「末尾」添加新元素,并且每次翻转的都是「整个」当前序列。 这启发我们:所谓「翻转」,本质上只改变了序列首尾的方向,而非元素本身。

核心洞察:引入一个布尔变量 $\text{is\_rev}$,表示当前序列逻辑上是否处于翻转状态。 当遇到 $\text{'o'}$ 需要翻转时,只需将 $\text{is\_rev}$ 取反($\text{is\_rev} \gets \lnot \text{is\_rev}$), 无需移动任何元素。配合双端队列,插入开销同样是 $O(1)$。

$2.3$ 插入逻辑

第 $k$ 步需要将元素 $k$ 追加到序列的逻辑末尾。根据 $\text{is\_rev}$ 的状态,逻辑末尾可能对应双端队列的物理头部或物理尾部:

$2.4$ 翻转与正确性分析

当 $S_k = \text{'o'}$ 时,执行 $\text{is\_rev} \gets \lnot \text{is\_rev}$。这等价于将整个序列的首尾方向翻转。因为插入操作始终追踪逻辑末尾, 所以翻转后下一次插入会自动「反方向」进行——这正是题目要求的行为。

正确性可从不变式角度理解:设 Deque 中元素从左到右按当前逻辑顺序排列。$\text{is\_rev}$ 取反后,逻辑顺序变为从右到左,等同于一次完整翻转。 后续插入依据新的 $\text{is\_rev}$ 走对应方向,维持不变式始终成立。

$3$. 算法思路 & 复杂度

$3.1$ 算法步骤

  1. 读入 $N$ 和字符串 $S$。
  2. 初始化双端队列 $\text{dq}$ 为空,懒标记 $\text{is\_rev} \gets \text{false}$。
  3. 对于 $k = 1, 2, \dots, N$:
    • 若 $\text{is\_rev} = \text{false}$,则 push_back(k);否则 push_front(k)
    • 若 $S_{k-1} = \text{'o'}$,则 $\text{is\_rev} \gets \lnot \text{is\_rev}$。
  4. 输出最终序列:
    • 若 $\text{is\_rev} = \text{false}$,从队头到队尾正向输出 $N$ 个元素。
    • 若 $\text{is\_rev} = \text{true}$,从队尾到队头逆向输出 $N$ 个元素。

$3.2$ 复杂度分析

项目复杂度说明
时间复杂度$O(N)$遍历 $N$ 次,每次 $O(1)$ 的插入和标记取反,最后 $O(N)$ 输出
空间复杂度$O(N)$双端队列存储 $N$ 个元素

相比暴力做法的 $O(N^2)$,正解利用「整体翻转 → 懒标记」的关键转化,将物理翻转的代价完全消除,从 $O(N^2)$ 直降至 $O(N)$, 在 $N = 5 \times 10^5$ 下轻松通过。

$4$. 完整代码

#include <iostream>
#include <string>
#include <deque>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    if (!(cin >> n)) return 0;
    string s;
    cin >> s;

    deque<int> dq;
    bool is_rev = false;

    for (int k = 1; k <= n; ++k) {
        if (!is_rev) {
            dq.push_back(k);
        } else {
            dq.push_front(k);
        }

        if (s[k - 1] == 'o') {
            is_rev = !is_rev;
        }
    }

    if (!is_rev) {
        for (int i = 0; i < n; ++i) {
            cout << dq[i] << (i == n - 1 ? "" : " ");
        }
    } else {
        for (int i = n - 1; i >= 0; --i) {
            cout << dq[i] << (i == 0 ? "" : " ");
        }
    }
    cout << "\n";

    return 0;
}

$5$. 样例说明

$5.1$ 样例 $1$:$N=5,\; S=\text{'ooxoo'}$

$k$$S_k$$\text{is\_rev}$
(插入前)
插入Deque 内容$\text{is\_rev}$
(操作后)
$1$$\text{'o'}$$\text{false}$push_back(1)$[1]$$\text{true}$
$2$$\text{'o'}$$\text{true}$push_front(2)$[2, 1]$$\text{false}$
$3$$\text{'x'}$$\text{false}$push_back(3)$[2, 1, 3]$$\text{false}$
$4$$\text{'o'}$$\text{false}$push_back(4)$[2, 1, 3, 4]$$\text{true}$
$5$$\text{'o'}$$\text{true}$push_front(5)$[5, 2, 1, 3, 4]$$\text{false}$

最终 $\text{is\_rev} = \text{false}$,正向输出:$[5, 2, 1, 3, 4]$,与样例输出一致。

$5.2$ 样例 $2$:$N=7,\; S=\text{'ooooooo'}$

$k$$S_k$$\text{is\_rev}$
(插入前)
插入Deque 内容$\text{is\_rev}$
(操作后)
$1$$\text{'o'}$$\text{false}$push_back(1)$[1]$$\text{true}$
$2$$\text{'o'}$$\text{true}$push_front(2)$[2, 1]$$\text{false}$
$3$$\text{'o'}$$\text{false}$push_back(3)$[2, 1, 3]$$\text{true}$
$4$$\text{'o'}$$\text{true}$push_front(4)$[4, 2, 1, 3]$$\text{false}$
$5$$\text{'o'}$$\text{false}$push_back(5)$[4, 2, 1, 3, 5]$$\text{true}$
$6$$\text{'o'}$$\text{true}$push_front(6)$[6, 4, 2, 1, 3, 5]$$\text{false}$
$7$$\text{'o'}$$\text{false}$push_back(7)$[6, 4, 2, 1, 3, 5, 7]$$\text{true}$

最终 $\text{is\_rev} = \text{true}$,逆向输出:$[7, 5, 3, 1, 2, 4, 6]$,与样例输出一致。

$5.3$ 样例 $3$:$N=15,\; S=\text{'xooxoxoxoxoxxoo'}$

$k$$S_k$$\text{is\_rev}$
(插入前)
插入方式$\text{is\_rev}$
(操作后)
$1$$\text{'x'}$$\text{false}$push_back$\text{false}$
$2$$\text{'o'}$$\text{false}$push_back$\text{true}$
$3$$\text{'o'}$$\text{true}$push_front$\text{false}$
$4$$\text{'x'}$$\text{false}$push_back$\text{false}$
$5$$\text{'o'}$$\text{false}$push_back$\text{true}$
$6$$\text{'x'}$$\text{true}$push_front$\text{true}$
$7$$\text{'o'}$$\text{true}$push_front$\text{false}$
$8$$\text{'x'}$$\text{false}$push_back$\text{false}$
$9$$\text{'o'}$$\text{false}$push_back$\text{true}$
$10$$\text{'x'}$$\text{true}$push_front$\text{true}$
$11$$\text{'o'}$$\text{true}$push_front$\text{false}$
$12$$\text{'x'}$$\text{false}$push_back$\text{false}$
$13$$\text{'x'}$$\text{false}$push_back$\text{false}$
$14$$\text{'o'}$$\text{false}$push_back$\text{true}$
$15$$\text{'o'}$$\text{true}$push_front$\text{false}$

最终 $\text{is\_rev} = \text{false}$,正向输出 Deque。模拟可得 Deque 最终为 $[15, 11, 10, 7, 6, 3, 1, 2, 4, 5, 8, 9, 12, 13, 14]$,与样例输出一致。

$6$. 注意事项与要点归纳

$6.1$ 最终输出方向

最容易遗漏的坑点:循环结束后 $\text{is\_rev}$ 可能为 $\text{true}$。这意味着 Deque 中元素在逻辑上是倒序的, 必须根据 $\text{is\_rev}$ 的状态决定从头输出还是从尾输出。若忽略这个判断直接正向输出,将得到错误答案。

$6.2$ 懒标记状态的正确追踪

懒标记 $\text{is\_rev}$ 表示当前序列是否处于翻转状态。插入操作和翻转操作依赖同一标记,顺序至关重要: 先基于当前标记插入 $k$,再根据 $S_k$ 翻转标记。若顺序颠倒,翻转操作的语义将偏移一位,导致结果错误。

$6.3$ 思维线索回顾

思考路径: 模拟翻转 → $O(N^2)$ 不可行 → 观察到每次翻转对象是「整个序列」→ 序列首尾方向在翻转前后对调 → 用布尔标记追踪方向,避免物理翻转 → 逻辑末尾对应物理头部或尾部的选择 → 双端队列 $O(1)$ 插入 → 最终根据标记输出方向输出结果 → $O(N)$ 正解。

$6.4$ 扩展思考


D. X to Y

普及+/提高− AtCoder LCA 数学 K叉树 最短路径 时间限制:$2$ s 空间限制:$1024$ MB $T \le 2 \times 10^5$

$1$. 题目大意

给定初始值 $X$、目标值 $Y$ 和参数 $K$($K \ge 2$)。每次操作可以将当前数 $x$ 变为 $y$,要求满足以下两个条件之一:

  1. $y = \left\lfloor \frac{x}{K} \right\rfloor$(除以 $K$ 并向下取整)
  2. $x = \left\lfloor \frac{y}{K} \right\rfloor$,即 $y \in [xK,\; xK + K - 1]$(逆操作:乘以 $K$ 并加上一个偏移量 $0 \sim K-1$)

求将 $X$ 变为 $Y$ 的最少操作次数。数据范围:$T \le 2 \times 10^5$,$0 \le X, Y \le 10^{18}$,$2 \le K \le 10^{18}$。题目保证在有限步内一定可以达成。

本质提炼:这道题表面上是一道代数运算题,但深入剖析两种操作的性质后,会发现它实际上是一道树上最短路径问题——整个状态空间构成了一棵以 $0$ 为根的 $K$ 叉树。

$2$. 算法分析

$2.1$ 操作的几何意义:$K$ 进制视角

仔细观察两种操作在 $K$ 进制下的含义:

$2.2$ 从代数到图论:$K$ 叉树模型

如果把每一个非负整数看作图上的一个节点,将上述转化关系看作边,会得到一个怎样的图?

核心结论:整个状态空间构成了一棵以 $0$ 为根的 $K$ 叉树。题目要求的"最少操作次数",就是求树上节点 $X$ 到节点 $Y$ 的最短路径长度。

$2.3$ LCA 模型与最短路径

在树上,两点之间的最短路径必然经过它们的最近公共祖先(LCA, Lowest Common Ancestor)。因此,答案即为:

$$\text{Ans} = \text{dist}(X, \text{LCA}) + \text{dist}(Y, \text{LCA})$$

其中 $\text{dist}(A, B)$ 表示树上节点 $A$ 到 $B$ 的路径长度(即深度差)。

$2.4$ 图解辅助理解

以样例 $1$ 为例($X=11,\; Y=9,\; K=3$),状态树的局部如下:

          0
       /  |  \
      0   1   2
         /|\
        3 4 5
       /|\
      9 10 11

$2.5$ 正确性证明

因为状态图是一棵无环连通图(树),树上两点之间的简单路径是唯一且最短的,因此通过寻找 LCA 计算出的距离必定是全局最优解,不存在其他"捷径"。该转化严格等价于原问题。

$3$. 算法思路 & 复杂度

$3.1$ 算法流程

  1. 读入 $X, Y, K$。
  2. 分别求出 $X$ 和 $Y$ 到根节点 $0$ 的完整路径:
    • 从 $X$ 出发,不断执行 $X \gets \lfloor X / K \rfloor$ 直到 $X = 0$,将每个值存入路径数组(逆序存储,数组末尾为根 $0$)。
    • 对 $Y$ 做同样处理。
  3. 从根节点端(数组末尾)开始,同时向叶子方向扫描两条路径,找到最后一个相等的节点位置——此即 LCA。
  4. 答案 = $X$ 路径中 LCA 之上的剩余节点数 + $Y$ 路径中 LCA 之上的剩余节点数。
  5. 输出答案。

$3.2$ 复杂度分析

项目复杂度说明
时间(单组)$O(\log_K(\max(X, Y)))$$X, Y \le 10^{18}$, $K \ge 2$,最大深度 $\approx 60$
时间(总计)$O(T \cdot \log_K(\max(X, Y)))$$T \le 2 \times 10^5$,约 $1.2 \times 10^7$ 次运算,轻松通过 $2$ s
空间(单组)$O(\log_K(\max(X, Y)))$存储两条路径,每条最长约 $60$ 个元素

由于 $K \ge 2$ 且 $X, Y \le 10^{18}$,树的深度上限为 $\log_2(10^{18}) \approx 60$。即使 $T = 2 \times 10^5$,总运算次数也仅有约 $1.2 \times 10^7$ 次,在 $2$ 秒时限内绰绰有余。

$4$. 完整代码

#include <iostream>
#include <vector>

using namespace std;

void solve() {
    long long X, Y, K;
    cin >> X >> Y >> K;

    vector<long long> pathX, pathY;

    // 1. 提取 X 到根节点 (0) 的完整路径
    long long currX = X;
    while (currX > 0) {
        pathX.push_back(currX);
        currX /= K;
    }
    pathX.push_back(0); // 确保根节点 0 被加入

    // 2. 提取 Y 到根节点 (0) 的完整路径
    long long currY = Y;
    while (currY > 0) {
        pathY.push_back(currY);
        currY /= K;
    }
    pathY.push_back(0); // 确保根节点 0 被加入

    // 3. 从根节点开始向下寻找最近公共祖先 (LCA)
    int i = pathX.size() - 1;
    int j = pathY.size() - 1;

    // 只要路径节点相同,说明还在公共祖先链上,继续向下移动
    while (i >= 0 && j >= 0 && pathX[i] == pathY[j]) {
        i--;
        j--;
    }

    // 4. 此时 i 和 j 分别指向 X 和 Y 在分叉后的剩余路径长度
    // 注意:因为数组下标从 0 开始,剩余元素的个数恰好是 i + 1 和 j + 1
    long long ans = (i + 1) + (j + 1);

    cout << ans << "\n";
}

int main() {
    // 优化 C++ 的 I/O 速度,防止在 2e5 的数据量下 TLE
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int T;
    if (cin >> T) {
        while (T--) {
            solve();
        }
    }
    return 0;
}

$5$. 样例说明

下面逐一演算题目给定的 $4$ 组样例:

$\#$$X$$Y$$K$路径与 LCA答案
$1$$11$$9$$3$ $X$ 路径:$11 \to 3 \to 1 \to 0$
$Y$ 路径:$9 \to 3 \to 1 \to 0$
LCA $= 3$,剩余 $(11)$ 长度 $1$,$(9)$ 长度 $1$
$2$
$2$$0$$0$$2$ 两者均为根节点,路径都为 $[0]$
LCA $= 0$,剩余长度均为 $0$
$0$
$3$$842$$180$$7$ $X$ 路径:$842 \to 120 \to 17 \to 2 \to 0$(深度 $4$)
$Y$ 路径:$180 \to 25 \to 3 \to 0$(深度 $3$)
LCA $= 0$,剩余长度 $4 + 3 = 7$
$7$
$4$$1948706013487601$$48019760148910476$$89014537$ $K$ 极大,两数深度均很小
通过 LCA 计算得最短路径长度为 $5$
$5$

样例 $1$ 详析:$X = 11$,$Y = 9$,$K = 3$。操作一(除以 $3$)相当于向父节点移动,操作二(乘以 $3$ 加偏移)相当于向子节点移动。$11$ 先除以 $3$ 得到 $3$(向上到父节点),再从 $3$ 乘以 $3$ 加 $0$ 得到 $9$(向下到子节点)。共 $2$ 步:$11 \xrightarrow{/3} 3 \xrightarrow{\times 3 + 0} 9$。

样例 $3$ 详析:$X = 842$,$Y = 180$,$K = 7$。两者在 $7$ 叉树上分别位于不同子树,LCA 为根 $0$。需要 $842$ 向上 $4$ 步到根,再从根向下 $3$ 步到 $180$,总计 $7$ 步。

$6$. 注意事项与要点归纳

$6.1$ 边界条件与防坑指南

$6.2$ 思维线索回顾

思考路径:代数操作 $\xrightarrow{\text{K进制视角}}$ 删末尾/加数位 $\xrightarrow{\text{图论建模}}$ K 叉树节点 $\xrightarrow{\text{树上最短路径}}$ LCA 问题 $\xrightarrow{\text{暴力求路径}}$ $O(\log_K \max(X,Y))$ 解法。

$6.3$ 扩展思考


E. Digit Circus

提高 AtCoder 数位DP 状态压缩 滚动数组 数学 取模 时间限制:$2$ s 空间限制:$1024$ MB

$1$. 题目大意

给定一个极大的整数 $N$($1 \le N \lt 10^{500}$),求在区间 $[1, N]$ 中,满足以下三个条件中恰好一个的整数 $x$ 的数量,答案对 $998244353$ 取模:

  1. $x$ 是 $3$ 的倍数。
  2. $x$ 的十进制表示中包含数字 $3$。
  3. $x$ 的十进制表示中恰好使用了 $3$ 种不同的数字。

题目保证了 $N$ 的十进制表示没有多余的前导零。

$2$. 算法分析

$2.1$ 从暴力到正解的破局点

如果采用暴力做法,需要从 $1$ 遍历到 $N$ 逐一判断每个数是否满足「恰好一个条件」,时间复杂度高达 $O(N \log_{10} N)$。面对 $N \lt 10^{500}$ 的极大数据范围($N$ 最多有 $500$ 位),暴力做法完全不可行。

关键洞察:当上限 $N$ 极其庞大(以字符串形式给出),且要求统计满足某些「数字构成特征」的整数个数时,这构成了使用数位 DP(Digit DP)的强烈暗示。

数位 DP 的核心思想是:从高位到低位逐位确定数字,同时用紧凑的状态压缩当前已构造前缀的信息,避免对每个数单独判断,从而将复杂度从 $O(N)$ 降到 $O(L \times \text{状态数})$($L$ 为 $N$ 的位数)。

$2.2$ 条件拆解与状态设计

我们需要在数位 DP 的每个状态中,能够判断当前已构造的数字前缀是否满足三个条件。为此设计以下状态维度:

条件 $1$($3$ 的倍数):一个数是 $3$ 的倍数,当且仅当其各位数字之和是 $3$ 的倍数。维护当前数字之和对 $3$ 取模的结果:

条件 $2$(包含数字 $3$)与条件 $3$(恰好 $3$ 种数字):这两个条件都与「使用了哪些数字」有关。使用一个 $10$ 位的二进制数(bitmask)来记录数字 $0 \sim 9$ 的出现情况:

数位 DP 基础状态:

$2.3$ 核心转移与边界处理

在填入当前位的数字 $d$ 时,转移规则如下:

所有转移累加方案数,并时刻对 $998244353$ 取模。

$2.4$ 统计答案

处理完所有位后,遍历所有状态。只统计 is_started = 1 的状态(排除 $x = 0$ 的情况)。对于每个状态,判断三个条件的满足情况:

若 $\text{cond1} + \text{cond2} + \text{cond3} = 1$,则将该状态的方案数累加入答案。

$3$. 算法思路 & 复杂度

$3.1$ 算法流程

  1. 读入 $N$ 作为字符串。
  2. 初始化滚动数组 dp[is_less][is_started][rem3][mask],其中 dp[0][0][0][0] = 1
  3. 从高位到低位遍历 $N$ 的每一位字符:
    • 获取当前位的上限数字 max_digit
    • 建立新的临时数组 next_dp 全零。
    • 枚举当前所有非零状态的转移:对 $d = 0 \dots \text{limit}$ 进行扩展,更新 nxt_lessnxt_startednxt_rem3nxt_mask
    • next_dp 复制回 dp(滚动数组优化)。
  4. 处理完所有位后,遍历 dp[is_less][1][rem3][mask],筛选恰好满足一个条件的状态,累加答案。
  5. 输出答案。

$3.2$ 复杂度分析

项目复杂度说明
状态总数$12288$$2 \times 2 \times 3 \times 1024 = 12288$ 种状态
单步转移$\le 10$ 分支每个状态最多枚举 $0 \sim 9$ 共 $10$ 个数字
时间复杂度$O(L \cdot 10 \cdot 12288)$$L \le 500$,总运算约 $6 \times 10^7$
空间复杂度$O(12288)$滚动数组优化,仅需两层 $2 \times 2 \times 3 \times 1024$ 的数组

总运算次数约 $6 \times 10^7$,在 $2$ 秒时限内可轻松通过。使用滚动数组将空间从 $O(L \times 12288)$ 降为 $O(12288)$,常数极小。

$4$. 完整代码

#include <iostream>
#include <string>
#include <vector>

using namespace std;

const int MOD = 998244353;

// 取模加法辅助函数
inline void add(int &a, int b) {
    a += b;
    if (a >= MOD) a -= MOD;
}

int main() {
    // 优化 I/O 速度
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    string N;
    if (!(cin >> N)) return 0;

    // 滚动数组设计:dp[is_less][is_started][rem3][mask]
    int dp[2][2][3][1024] = {0};
    
    // 初始状态:还未填任何数字
    dp[0][0][0][0] = 1;

    // 从高位到低位逐个确定数字
    for (char c : N) {
        int max_digit = c - '0';
        int next_dp[2][2][3][1024] = {0}; // 下一个状态

        for (int is_less = 0; is_less < 2; ++is_less) {
            for (int is_started = 0; is_started < 2; ++is_started) {
                for (int rem3 = 0; rem3 < 3; ++rem3) {
                    for (int mask = 0; mask < 1024; ++mask) {
                        
                        if (!dp[is_less][is_started][rem3][mask]) continue;

                        // 确定当前位可以填入的最大数字
                        int limit = is_less ? 9 : max_digit;
                        
                        for (int d = 0; d <= limit; ++d) {
                            int nxt_less = is_less | (d < limit);
                            int nxt_started = is_started | (d > 0);
                            int nxt_rem3 = (rem3 + d) % 3;
                            int nxt_mask = mask;
                            
                            // NOTE: 只有当数字已经开始(非前导零)时,才将数字 d 计入掩码
                            if (nxt_started) {
                                nxt_mask |= (1 << d);
                            }
                            
                            add(next_dp[nxt_less][nxt_started][nxt_rem3][nxt_mask], 
                                dp[is_less][is_started][rem3][mask]);
                        }
                    }
                }
            }
        }
        
        // 滚动数组:将 next_dp 覆盖到 dp,空间重用
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 2; ++j) {
                for (int k = 0; k < 3; ++k) {
                    for (int m = 0; m < 1024; ++m) {
                        dp[i][j][k][m] = next_dp[i][j][k][m];
                    }
                }
            }
        }
    }

    int ans = 0;
    
    // 统计最终符合条件的答案
    for (int is_less = 0; is_less < 2; ++is_less) {
        for (int rem3 = 0; rem3 < 3; ++rem3) {
            for (int mask = 0; mask < 1024; ++mask) {
                
                // 只统计合法的数字 (is_started == 1 排除了 0)
                if (!dp[is_less][1][rem3][mask]) continue;

                // 拆解三大条件
                bool cond1 = (rem3 == 0);                               // 是 3 的倍数
                bool cond2 = ((mask & (1 << 3)) != 0);                  // 包含数字 3
                bool cond3 = (__builtin_popcount(mask) == 3);           // 恰好 3 种不同的数字

                // 题目要求:满足【恰好一个】条件
                int count_conditions = cond1 + cond2 + cond3;
                if (count_conditions == 1) {
                    add(ans, dp[is_less][1][rem3][mask]);
                }
            }
        }
    }

    cout << ans << "\n";
    return 0;
}

$5$. 样例说明

$\#$$N$答案说明
$1$ $45$ $19$ 仅满足条件 $1$($3$ 的倍数):$6, 9, 12, 15, 18, 21, 24, 27, 42, 45$,共 $10$ 个。
仅满足条件 $2$(含数字 $3$):$13, 23, 31, 32, 34, 35, 37, 38, 43$,共 $9$ 个。
仅满足条件 $3$(恰好 $3$ 种数字):无(区间内不足 $3$ 种不同数字或同时命中其他条件)。
合计 $10 + 9 + 0 = 19$。
$2$ $1013$ $424$ 条件 $1$ 示例:$555$($5+5+5=15$,$3$ 的倍数,但不含 $3$ 且用了 $1$ 种数字)、$1011$。
条件 $2$ 示例:$343$(含 $3$,但非 $3$ 的倍数且用了 $2$ 种数字)、$553$。
条件 $3$ 示例:$1012$(用了 $\{0,1,2\}$ 三种数字,非 $3$ 的倍数且不含 $3$)、$704$。
共 $424$ 个。
$3$ $2$ $0$ $1$ 不满足任何条件;$2$ 不满足任何条件。答案为 $0$。
$4$ $314159265358979323846264338327950$ $658111391$ 极大的 $N$($33$ 位),暴力完全不可行。数位 DP 在约 $6 \times 10^7$ 次运算内得出结果。

样例 $1$ 详细演算:以 $N = 45$ 为例,手动验证几个典型数字:

$6$. 注意事项与要点归纳

$6.1$ 易错点与边界

$6.2$ 思维线索回顾

思考路径:$N \lt 10^{500}$ 极大 → 暴力不可行 → 数字构成特征统计 → 数位 DP 强烈暗示 → 条件 $1$ 用 $\text{rem3}$ + 条件 $2/3$ 用 $\text{mask}$ 的 $10$ 位 bitmask → 前导零用 is_started 排除 → 滚动数组优化空间 → 最终统计「恰好一个」条件。

$6.3$ 扩展思考


F. Sjeltzer?

提高+/省选- AtCoder 高维前缀和 SOS DP 容斥原理 状态压缩 前缀和 时间限制:$2$ s 空间限制:$1024$ MB

$1$. 题目大意

冰箱中有 $N$ 瓶饮料,每瓶饮料有一个 $6$ 位数字编号 $S_i$(可含前导零)和对应的体积 $V_i$。所有 $S_i$ 两两不同。给定 $Q$ 次查询,每次给出两个 $6$ 位数字字符串 $x$ 和 $y$,要求计算所有满足「对每个数位 $k$ 都有 $x_k \le (S_i)_k \le y_k$」的饮料体积之和。

数据范围:$N, Q \le 3 \times 10^5$,$V_i \le 10^9$。

本题本质是一个 $6$ 维空间下的区间求和问题。每一维(即每个数位)的坐标范围仅为 $0 \sim 9$,整个 $6$ 维空间总共只有 $10^6$ 种可能的坐标状态,远小于 $N$ 和 $Q$。

$2$. 算法分析

$2.1$ 暴力做法

对于每次查询,遍历所有 $N$ 瓶饮料,逐位比对是否满足 $x_k \le (S_i)_k \le y_k$。单次查询时间复杂度 $O(N)$,总复杂度 $O(NQ)$。代入 $N, Q \le 3 \times 10^5$,最坏运算次数接近 $10^{11}$,必然超时。必须将单次查询优化到更低量级。

$2.2$ 状态压缩:$6$ 维到 $1$ 维

既然完整状态空间大小只有 $10^6$,我们可以把每个 $6$ 位编号 $d_1 d_2 d_3 d_4 d_5 d_6$ 映射为一个整数索引 $idx = \overline{d_1 d_2 d_3 d_4 d_5 d_6}$(即直接视为 $0 \sim 999999$ 的十进制数)。定义数组 $A[idx]$ 为编号恰好等于该值的饮料体积之和。那么原问题转化为:在 $6$ 维数组上快速求一个 $6$ 维超矩形内的元素之和。

$2.3$ SOS DP 构建高维前缀和

设前缀和 $P[idx]$ 表示所有坐标 $c_k \le d_k$($k = 1, \dots, 6$)的状态对应的体积总和。如何高效构建这个 $6$ 维前缀和?答案是逐维累加——这正是 SOS DP(Sum Over Subsets / 高维前缀和)的核心思想。

依次处理 $6$ 个维度。对于第 $dim$ 维(权重为 $10^{dim}$),遍历所有 $10^6$ 个状态:若当前状态在该维度的坐标为 $digit \gt 0$,则累加 $digit - 1$ 处(即减去当前维度的 $1$ 个单位)的值:

$$P[i] \gets P[i] + P[i - 10^{dim}]$$

这样,经过 $6$ 轮扫描(每轮 $10^6$ 次运算),总预处理量仅 $6 \times 10^6$,数组 $P$ 中每个位置就存储了其对应的 $6$ 维前缀和。

关键洞察:SOS DP 的「逐维累加」保证了每个维度上的前缀和独立性——先处理维度 $0$,此时 $P[i]$ 表示维度 $0$ 上的前缀和;再处理维度 $1$,此时 $P[i]$ 表示维度 $0,1$ 上的二维前缀和;依次类推,$6$ 轮后即得完整的 $6$ 维前缀和。

$2.4$ 容斥原理查询

有了 $6$ 维前缀和,如何回答 $6$ 维区间 $\prod_{k=1}^{6} [x_k,\, y_k]$ 的求和?这是经典的高维容斥原理。

在 $1$ 维中,区间 $[x, y]$ 为 $P[y] - P[x - 1]$。推广到 $6$ 维,每个维度有两种选择:取上界 $y_k$(符号 $+$)或取下界 $x_k - 1$(符号 $-$)。总共 $2^6 = 64$ 种组合,每种组合对应一个容斥角点。该角点的符号为 $(-1)^{\text{取下界的维度数}}$。

形式化地,令 $mask \in [0, 63]$ 表示 $6$ 位的二进制选择($0$ = 取上界 $y_k$,$1$ = 取下界 $x_k - 1$),则:

$$\text{Ans} = \sum_{mask=0}^{63} (-1)^{\text{popcount}(mask)} \cdot P\!\left[\text{idx}(mask)\right]$$

其中 $\text{idx}(mask)$ 是将各维度的选择拼接而成的 $6$ 位坐标值。若某维度取 $x_k - 1$ 后变成 $-1$(即 $x_k = 0$ 时),则该角点越界,贡献为零,直接跳过。

单次查询仅需遍历 $64$ 种组合,时间复杂度 $O(2^6) = O(1)$。

$3$. 算法思路 & 复杂度

$3.1$ 算法流程

  1. 读入所有饮料数据,将 $S_i$ 转为整数索引 $idx$,累加 $A[idx] \mathrel{+}= V_i$。
  2. SOS DP 构建前缀和:对 $dim = 0 \sim 5$,遍历 $i = 0 \sim 999999$,若第 $dim$ 位数字 $\gt 0$,执行 $P[i] \mathrel{+}= P[i - 10^{dim}]$。
  3. 处理查询:
    • 若存在任意维度 $x_k \gt y_k$,直接输出 $0$(区间非法)。
    • 否则枚举 $mask = 0 \sim 63$,构造角点坐标:
      • bit $= 0$ → 取 $y_k$(正号)
      • bit $= 1$ → 取 $x_k - 1$(负号),若结果 $\lt 0$ 则跳过
    • 累加 $sign \times P[idx]$,其中 $sign = (-1)^{\text{popcount}(mask)}$。
  4. 输出答案。

$3.2$ 复杂度分析

项目复杂度说明
预处理(SOS DP)$O(D \cdot 10^D)$$D = 6$,约 $6 \times 10^6$ 次运算
单次查询$O(2^D)$$2^6 = 64$ 次运算
查询总计$O(Q \cdot 2^D)$$Q \le 3 \times 10^5$,约 $1.92 \times 10^7$ 次
总体时间$O(D \cdot 10^D + Q \cdot 2^D)$约 $2.5 \times 10^7$ 次运算,远在 $2$ s 内
空间$O(10^D)$一个 $\texttt{long long}$ 数组,约 $8$ MB

$4$. 完整代码

#include <iostream>
#include <string>
#include <vector>

using namespace std;

long long sum[1000000];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    if (!(cin >> N)) return 0;

    for (int i = 0; i < N; ++i) {
        string s;
        long long v;
        cin >> s >> v;
        sum[stoi(s)] += v;
    }

    for (int dim = 0; dim < 6; ++dim) {
        int p = 1;
        for (int k = 0; k < dim; ++k) p *= 10;
        
        for (int i = 0; i < 1000000; ++i) {
            int digit = (i / p) % 10;
            if (digit > 0) {
                sum[i] += sum[i - p];
            }
        }
    }

    int Q;
    cin >> Q;
    
    while (Q--) {
        string x, y;
        cin >> x >> y;

        bool valid = true;
        for (int i = 0; i < 6; ++i) {
            if (x[i] > y[i]) {
                valid = false;
                break;
            }
        }
        if (!valid) {
            cout << 0 << "\n";
            continue;
        }

        long long ans = 0;
        
        for (int mask = 0; mask < 64; ++mask) {
            int current_idx = 0;
            int sign = 1;
            bool out_of_bounds = false;

            for (int dim = 0; dim < 6; ++dim) {
                int bit = (mask >> dim) & 1;
                int digit;
                
                if (bit == 1) { 
                    digit = (x[dim] - '0') - 1;
                    sign = -sign;
                } else { 
                    digit = y[dim] - '0';
                }

                if (digit < 0) {
                    out_of_bounds = true;
                    break;
                }
                
                current_idx = current_idx * 10 + digit;
            }

            if (!out_of_bounds) {
                ans += sign * sum[current_idx];
            }
        }
        cout << ans << "\n";
    }

    return 0;
}

$5$. 样例说明

$5.1$ 样例 $1$ 逐条演算

共有 $5$ 瓶饮料:$\texttt{000000}(1)$, $\texttt{314159}(2)$, $\texttt{161803}(10)$, $\texttt{169231}(5)$, $\texttt{384400}(20)$。$4$ 次查询:

$\#$$x$$y$匹配饮料逻辑答案
$1$$\texttt{150001}$$\texttt{269944}$ $\texttt{161803}: 10$
$\texttt{169231}: 5$
首两位 $15 \sim 26$,匹配 $16$ 开头;后四位均满足区间 $15$
$2$$\texttt{302010}$$\texttt{396939}$ (无) 首两位 $30 \sim 39$,所有饮料首两位均不在此区间 $0$
$3$$\texttt{000000}$$\texttt{999999}$ 全部 $5$ 瓶 全空间,$1 + 2 + 10 + 5 + 20 = 38$ $38$
$4$$\texttt{999000}$$\texttt{000444}$ (无) $x_k \gt y_k$(首维 $9 \gt 0$),区间非法 $0$

$5.2$ 样例 $2$ 逐条演算

共 $8$ 瓶饮料,编号及体积见题面。$8$ 次查询结果如下表所示。其中查询 $7$($\texttt{101033} \sim \texttt{999499}$)匹配了 $5$ 瓶,答案 $584885376$ 为这些饮料体积之和;查询 $4,5,6,8$ 均仅匹配到 $\texttt{454854}$(体积 $331175626$),因为 $x$ 的范围恰以 $\texttt{454854}$ 为中心浮动。

$\#$$x$$y$匹配数量答案
$1$$\texttt{201000}$$\texttt{785589}$$4$$515024780$
$2$$\texttt{202325}$$\texttt{955898}$$1$$109900930$
$3$$\texttt{310401}$$\texttt{875947}$$0$$0$
$4$$\texttt{044023}$$\texttt{988999}$$1$$331175626$
$5$$\texttt{111230}$$\texttt{567897}$$1$$331175626$
$6$$\texttt{133241}$$\texttt{577989}$$1$$331175626$
$7$$\texttt{101033}$$\texttt{999499}$$5$$584885376$
$8$$\texttt{453013}$$\texttt{796988}$$1$$331175626$

样例 $1$ 查询 $4$ 详析:$x = \texttt{999000}$,$y = \texttt{000444}$。仅看首维:$x_1 = 9$,$y_1 = 0$,出现 $x_k \gt y_k$。这是容斥前必须拦截的边界情况——数学上该区间为空集,直接返回 $0$ 而非让容斥角点产生负数坐标。

$6$. 注意事项与要点归纳

$6.1$ 边界陷阱与易错点

$6.2$ 思维线索回顾

思考路径: $6$ 维区间查询 → 状态空间仅 $10^6$(可行的预处理规模) → 高维前缀和 = 逐维累加(SOS DP) → 查询用 $6$ 维容斥展开为 $2^6 = 64$ 个角点求和 → $O(10^6 \cdot D + Q \cdot 2^D)$ 完美通过。

$6.3$ 扩展思考


G. Sum of Mex of Mod of Linear

省选 AtCoder 数学 数论 类欧几里得算法 数据结构 离线处理 环上问题转化 时间限制:$3$ s 空间限制:$1024$ MB

$1$. 题目大意

给定长度为 $N$ 的数组 $A$ 和参数 $M, C, K$。你需要处理 $Q$ 次单点修改查询:第 $q$ 次查询将 $A_{i_q}$ 改为 $X_q$,并求以下式子的值:

$$\sum_{k=0}^{K-1} \text{mex}_{1 \le i \le N} \{ (Ck + A_i) \pmod M \}$$

其中 $\text{mex}$ 表示集合中未出现的最小非负整数。约束:$1 \le N, Q \le 2 \times 10^5$,$0 \le C \lt M \le 10^9$,$1 \le K \le 10^9$,$0 \le A_i, X_q \lt M$。

$2$. 算法分析

$2.1$ 从集合 MEX 到区间长度

直接维护集合并求 $\text{mex}$ 的代价极高。让我们转换视角:求 $\text{mex}(S_k) = v$ 等价于说 $0, 1, \dots, v-1$ 都在集合 $S_k$ 中,而 $v \notin S_k$。

因为 $S_k$ 中的元素形如 $(A_i + Ck) \bmod M$,所以 $x \in S_k \iff (x - Ck) \bmod M \in A$。 换言之,如果在原始集合 $A$ 中,从 $(-Ck) \bmod M$ 开始顺时针走,连续出现的元素段长度为 $L$,那么 $\text{mex}(S_k)$ 就恰好等于 $L$。

核心结论:令 $A$ 包含的去重元素构成集合 $B$。对于一个固定的起点 $z_k = (-Ck) \bmod M$,其对应的 MEX 值,等于集合 $B$ 中以 $z_k$ 为起点的连续段长度(若 $z_k \notin B$ 则长度为 $0$)。

$2.2$ 交换求和顺序

既然单次 MEX 对应一段连续区间,我们不妨对集合 $B$ 形成的连续区间进行统计。 假设集合 $B$ 在环上形成了一个极大连续区间 $[u, v]$(包含端点),其长度为 $len$。 对于数列 $z_k = (Dk) \bmod M$(这里令 $D = (M - C \bmod M) \bmod M$),当且仅当 $z_k$ 落在区间 $[u, v]$ 内时,它对答案有贡献,且贡献值为 $(v - z_k + 1) \bmod M$。

那么该区间对总答案的贡献可以写为:

$$\sum_{k=0}^{K-1} [z_k \in [u, v]] \times (\text{从 } z_k \text{ 到 } v \text{ 的距离} + 1)$$

为了将环上区间转化为线性比较,我们对 $z_k$ 做平移映射,令 $z'_k = (z_k - u) \bmod M$。此时条件 $z_k \in [u, v]$ 等价于 $z'_k \lt len$,而它对答案的贡献正好是 $len - z'_k$。 因此,区间 $[u, v]$ 的总贡献简化为:

$$\sum_{k=0}^{K-1} [z'_k \lt len] (len - z'_k)$$

$2.3$ 拆解为扩展类欧几里得算法

注意到 $z'_k = (Dk + M - u) \bmod M$。令 $B_{val} = (M - u) \bmod M$,则有:

$$z'_k = Dk + B_{val} - M \left\lfloor \frac{Dk + B_{val}}{M} \right\rfloor$$

同时,条件指示函数 $[z'_k \lt len]$ 可以用两个取整函数的差值来表示。令 $B_{prime} = B_{val} + M - len$,则:

$$[z'_k \ge len] = \left\lfloor \frac{Dk + B_{prime}}{M} \right\rfloor - \left\lfloor \frac{Dk + B_{val}}{M} \right\rfloor$$

令 $I_k = [z'_k \ge len]$,那么我们需要求的目标指示函数 $J_k = [z'_k \lt len] = 1 - I_k$。

将 $z'_k$ 代入区间贡献公式中,展开并整理可得:

$$\text{Sum} = (len - B_{val}) \sum_{k=0}^{K-1} J_k - D \sum_{k=0}^{K-1} k J_k + M \sum_{k=0}^{K-1} J_k \left\lfloor \frac{Dk + B_{val}}{M} \right\rfloor$$

经过数学推导,我们只需要实现一个能同时求解以下三个值的扩展类欧几里得算法(Extended Floor Sum)

  1. $f = \sum \left\lfloor \frac{ai+b}{c} \right\rfloor$
  2. $g = \sum i \left\lfloor \frac{ai+b}{c} \right\rfloor$
  3. $h = \sum \left\lfloor \frac{ai+b}{c} \right\rfloor^2$

求出对应参数下的 $\Delta f, \Delta g, \Delta h$,即可在 $O(\log M)$ 的时间内计算出任意一个区间对总 MEX 的贡献。

$2.4$ 边界处理与区间维护

有了 $O(\log M)$ 算出单段区间贡献的工具,剩下的就是数据结构部分:

这一设计完美避开了环形区间分裂合并时的大量 Corner Cases。

$3$. 算法思路 & 复杂度

$3.1$ 算法流程

  1. 读入 $N, M, C, K$,计算 $D = (M - C \bmod M) \bmod M$。
  2. 读入数组 $A$,用 std::map 维护频次 $freq$ 和线性极大连续段 $segs$,初始构建贡献和 $linear\_sum$。
  3. 对每次查询 $(i_q, X_q)$:
    • 将旧值 $A_{i_q}$ 的频次减 $1$,若频次归零则从区间维护中移除该元素;
    • 将新值 $X_q$ 的频次加 $1$,若首次出现则加入区间维护;
    • 更新 $A_{i_q} \gets X_q$;
    • 调用 get_total_ans() 计算答案并输出。

其中 calc_B_interval(u, v) 函数利用扩展类欧几里得算法,在 $O(\log M)$ 内计算单个区间 $[u, v]$ 对答案的贡献。

$3.2$ 复杂度分析

项目复杂度说明
时间(预处理)$O(N \log M)$每个元素插入至多影响常数个区间,每次重新计算贡献需调用一次 ext_floor_sum
时间(单次查询)$O(\log M)$删除 + 插入至多影响常数个区间
时间(总计)$O((N + Q) \log M)$$N, Q \le 2 \times 10^5$, $M \le 10^9$,在 $3$ 秒时限下绰绰有余
空间$O(N + Q)$std::map 与频次数组仅记录出现的元素

$4$. 完整代码

#include <iostream>
#include <vector>
#include <map>

using namespace std;
using i128 = __int128_t;

long long N, M, C, K;
long long D;

struct Result {
    i128 f, g, h;
};

// 扩展类欧几里得算法
Result ext_floor_sum(long long n_in, long long a_in, long long b_in, long long c_in) {
    i128 n = n_in, a = a_in, b = b_in, c = c_in;
    if (a == 0) {
        i128 q = b / c;
        return {n * q, n * (n - 1) / 2 * q, n * q * q};
    }
    if (a >= c || b >= c) {
        i128 q_a = a / c, q_b = b / c;
        i128 rem_a = a % c, rem_b = b % c;
        Result res = ext_floor_sum(n_in, (long long)rem_a, (long long)rem_b, (long long)c);
        i128 sum_i = n * (n - 1) / 2;
        i128 sum_i2 = n * (n - 1) * (2 * n - 1) / 6;

        i128 f = res.f + q_a * sum_i + q_b * n;
        i128 g = res.g + q_a * sum_i2 + q_b * sum_i;
        i128 h = res.h + q_a * q_a * sum_i2 + q_b * q_b * n
                 + 2 * q_a * q_b * sum_i + 2 * q_a * res.g + 2 * q_b * res.f;
        return {f, g, h};
    }

    i128 m = (a * (n - 1) + b) / c;
    if (m == 0) return {0, 0, 0};

    Result res = ext_floor_sum((long long)m, (long long)c, (long long)(c - b - 1), (long long)a);

    i128 f = (n - 1) * m - res.f;
    i128 g = (m * n * (n - 1) - res.h - res.f) / 2;
    i128 h = (n - 1) * m * m - 2 * res.g - res.f;

    return {f, g, h};
}

// 计算环上单段存在元素 [u, v] 产生的 MEX 总贡献
i128 calc_B_interval(long long u, long long v) {
    long long len = (v - u + M) % M + 1;
    if (len == M) return (i128)K * M;

    long long B_val = (M - u) % M;
    long long B_prime = B_val + M - len;

    Result res1 = ext_floor_sum(K, D, B_prime, M);
    Result res2 = ext_floor_sum(K, D, B_val, M);

    i128 df = res1.f - res2.f;
    i128 dg = res1.g - res2.g;
    i128 dh = res1.h - res2.h;

    i128 sum_J = K - df;
    i128 sum_kJ = (i128)K * (K - 1) / 2 - dg;
    i128 sum_JY = res2.f - (dh - df) / 2;

    return (i128)(len - B_val) * sum_J - (i128)D * sum_kJ + (i128)M * sum_JY;
}

map<long long, long long> segs;
map<long long, long long> freq;
i128 linear_sum = 0;

void add_element(long long x) {
    auto it = segs.upper_bound(x);
    bool merge_left = false, merge_right = false;
    long long L = x, R = x;
    auto prev_it = it;

    if (it != segs.begin()) {
        prev_it = prev(it);
        if (prev_it->second >= x) return;
        if (prev_it->second == x - 1) {
            merge_left = true;
            L = prev_it->first;
        }
    }
    if (it != segs.end()) {
        if (it->first == x + 1) {
            merge_right = true;
            R = it->second;
        }
    }

    if (merge_left) { linear_sum -= calc_B_interval(prev_it->first, prev_it->second); segs.erase(prev_it); }
    if (merge_right) { linear_sum -= calc_B_interval(it->first, it->second); segs.erase(it); }

    segs[L] = R;
    linear_sum += calc_B_interval(L, R);
}

void remove_element(long long x) {
    auto it = segs.upper_bound(x);
    if (it != segs.begin()) {
        it = prev(it);
        if (it->first <= x && x <= it->second) {
            long long L = it->first, R = it->second;
            linear_sum -= calc_B_interval(L, R);
            segs.erase(it);
            if (L <= x - 1) {
                segs[L] = x - 1;
                linear_sum += calc_B_interval(L, x - 1);
            }
            if (x + 1 <= R) {
                segs[x + 1] = R;
                linear_sum += calc_B_interval(x + 1, R);
            }
        }
    }
}

i128 get_total_ans() {
    if (segs.empty()) return 0;
    auto first = segs.begin();
    auto last = prev(segs.end());

    if (first->first == 0 && first->second == M - 1) {
        return (i128)K * M;
    }
    if (first->first == 0 && last->second == M - 1) {
        i128 ans = linear_sum;
        ans -= calc_B_interval(0, first->second);
        ans -= calc_B_interval(last->first, M - 1);
        ans += calc_B_interval(last->first, first->second);
        return ans;
    }
    return linear_sum;
}

void print(i128 x) {
    if (x == 0) { cout << 0 << "\n"; return; }
    string s;
    while (x > 0) {
        s += (char)('0' + (x % 10));
        x /= 10;
    }
    for (int i = 0; i < s.length() / 2; i++) swap(s[i], s[s.length() - 1 - i]);
    cout << s << "\n";
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    long long Q;
    if (!(cin >> N >> M >> C >> K)) return 0;

    D = (M - C % M) % M;
    vector<long long> A(N + 1);

    for (int i = 1; i <= N; i++) {
        cin >> A[i];
        freq[A[i]]++;
        if (freq[A[i]] == 1) add_element(A[i]);
    }

    cin >> Q;
    for (int q = 1; q <= Q; q++) {
        long long idx, val;
        cin >> idx >> val;

        long long old_val = A[idx];
        freq[old_val]--;
        if (freq[old_val] == 0) remove_element(old_val);

        A[idx] = val;
        freq[val]++;
        if (freq[val] == 1) add_element(val);

        print(get_total_ans());
    }

    return 0;
}

$5$. 样例说明

$5.1$ 样例 $1$:$N=2,\; M=3,\; C=1,\; K=2$

初始 $A = (0, 2)$,$D = (3 - 1 \bmod 3) \bmod 3 = 2$。以下对每个查询进行演算:

$\#$$A$$B$(去重集合)$z_0 = 0$$z_1 = (2 \times 1) \bmod 3 = 2$$\text{mex}$ 和答案
$1$$(0, 2)$$\{0, 2\}$ 起点 $0$,连续段 $\{0\}$ 长 $1$ 起点 $2$,连续段 $\{2\}$ 长 $1$ $1 + 2 = 3$ $3$
$2$$(1, 2)$$\{1, 2\}$ $0 \notin B$,$\text{mex}=0$ 起点 $2$,连续段 $\{2\}$ 长 $1$ $0 + 1 = 1$ $1$
$3$$(1, 1)$$\{1\}$ $0 \notin B$,$\text{mex}=0$ $2 \notin B$,$\text{mex}=0$ $0 + 0 = 0$ $0$

查询 $1$ 详析:$A = (0, 2)$,去重后 $B = \{0, 2\}$。$D = 2$,$z_0 = 0$,$z_1 = 2$。对于 $z_0 = 0$,在 $B$ 中从 $0$ 开始顺时针走,$0 \in B$,但紧接的 $1 \notin B$,连续段长度 $= 1$,$\text{mex}=1$。对于 $z_1 = 2$,$2 \in B$,但 $0 \notin B$(绕回后),连续段长度 $= 1$,$\text{mex} = 2$(因为 $0, 1 \notin S_1$,注意 $S_1 = \{(C \cdot 1 + A_i) \bmod 3\} = \{(1+0)\bmod3, (1+2)\bmod3\} = \{1, 0\}$,$\text{mex}=2$)。

$5.2$ 样例 $2$:$N=7,\; M=9,\; C=3,\; K=19$

$\#$修改$A$ 数组答案
$1$$i=3,\; X=5$$[7, 0, 5, 8, 1, 3, 4]$$32$
$2$$i=1,\; X=6$$[6, 0, 5, 8, 1, 3, 4]$$44$
$3$$i=3,\; X=2$$[6, 0, 2, 8, 1, 3, 4]$$53$
$4$$i=5,\; X=5$$[6, 0, 2, 8, 5, 3, 4]$$37$
$5$$i=4,\; X=7$$[6, 0, 2, 7, 5, 3, 4]$$49$
$6$$i=7,\; X=8$$[6, 0, 2, 7, 5, 3, 8]$$37$
$7$$i=2,\; X=4$$[6, 4, 2, 7, 5, 3, 8]$$54$

以查询 $1$ 为例说明计算过程:$A$ 中去重后 $B = \{0, 1, 3, 4, 5, 7, 8\}$。$D = (9 - 3 \bmod 9) \bmod 9 = 6$。线性区间有 $\{0, 1\}, \{3, 4, 5\}, \{7, 8\}$(注意在模 $9$ 的环上 $\{7, 8\}$ 和 $\{0, 1\}$ 实际上可能相连,需通过首尾合并特判修正)。对 $k=0$ 到 $18$ 计算 $z_k = (6k) \bmod 9$ 的轨迹为 $0, 6, 3, 0, 6, 3, \dots$(每 $3$ 步循环),分别落点于各区间时按贡献公式累加,最终得到 $32$。

$6$. 注意事项与要点归纳

$6.1$ 易错点与边界

$6.2$ 思维线索回顾

思考路径:直接求 MEX 困难 → 转化为环上连续段长度 → 交换求和顺序,按区间统计 → 平移映射简化区间条件 → 指示函数用整除拆解 → 推导为三个扩展类欧求和 → $O(\log M)$ 计算单区间贡献 → std::map 维护动态区间 → 环上首尾合并容斥 → $O((N+Q) \log M)$ 总复杂度。

$6.3$ 扩展思考