给定两个正整数 $A$ 和 $B$($1 \le A, B \le 1000$),判断 $A$ 是否严格大于 $B$ 的三分之二,即判断 $A > B \times \frac{2}{3}$ 是否成立。若成立则输出 Yes,否则输出 No。
最直接的做法是计算 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)。
为了彻底规避浮点数精度问题,应将公式转化为纯整数运算。在原不等式 $$A > B \times \frac{2}{3}$$ 两边同时乘以正数 $3$(不等号方向不变),可得完全等价的整数形式: $$3 \times A > 2 \times B$$ 通过这一简单的数学变形,除法和浮点乘法被替换为纯粹的整数乘法和比较。整数运算不仅速度更快,而且结果绝对精确。
No。若误用了 $\ge$,该边界样例将返回错误结果。int 类型的上限(约 $2.1 \times 10^9$),不会发生整数溢出。若数据范围扩展至 $10^9$ 级别,则 $3A$ 可达 $3 \times 10^9$,建议改用 long long。Yes;否则输出 No。| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | $O(1)$ | 仅涉及常数次乘法和比较运算 |
| 空间复杂度 | $O(1)$ | 仅需存储 $A$ 和 $B$ 两个变量 |
#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;
}
题目提供了 $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$ 为假,结论一致。
int 完全够用,$3A \le 3000$。int 上界(约 $2.1 \times 10^9$),此时应改用 long long 避免溢出。某停车场分段计费:在 $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$。输出一个整数表示总费用。
由于时间范围极小($A, B \le 23$,最多 $22$ 个小时),最直接的方法是模拟停车期间的每一个小时:
此方法清晰直观,时间复杂度 $O(B - A)$,在本题数据范围下可以轻松通过(AC)。但思考:如果停车时长跨度极大(如 $B - A$ 可达 $10^9$),循环模拟就会超时(TLE)。
为了得到不依赖时间跨度的常数时间算法,我们将问题抽象为求两个线段的交集长度:
由此可得:
该解法仅涉及 $O(1)$ 次基本运算,与 $B - A$ 的大小完全无关。
区间交集公式能够覆盖两区间之间所有可能的位置关系:
关于数据溢出:$X, Y \le 1000$,时长最多 $B - A \le 22$,总费用最大约为 $22 \times 1000 = 22000$,完全在 $32$ 位有符号整数 $\texttt{int}$ 的表示范围内,无需担心溢出。
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | $O(1)$ | 仅涉及常数的加减法和最值运算,与停车时长无关 |
| 空间复杂度 | $O(1)$ | 仅使用几个整型变量存储输入和中间结果 |
#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;
}
下面逐条演算所有 $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$。
初始序列 $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$ 个整数,空格分隔,表示最终序列。
最直观的做法是用 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)。
暴力做法的瓶颈在于每一次 $S_k = \text{'o'}$ 都要进行 $O(k)$ 的物理翻转。 仔细观察操作特性:每次只在当前序列的「末尾」添加新元素,并且每次翻转的都是「整个」当前序列。 这启发我们:所谓「翻转」,本质上只改变了序列首尾的方向,而非元素本身。
第 $k$ 步需要将元素 $k$ 追加到序列的逻辑末尾。根据 $\text{is\_rev}$ 的状态,逻辑末尾可能对应双端队列的物理头部或物理尾部:
push_back(k)。push_front(k)。当 $S_k = \text{'o'}$ 时,执行 $\text{is\_rev} \gets \lnot \text{is\_rev}$。这等价于将整个序列的首尾方向翻转。因为插入操作始终追踪逻辑末尾, 所以翻转后下一次插入会自动「反方向」进行——这正是题目要求的行为。
正确性可从不变式角度理解:设 Deque 中元素从左到右按当前逻辑顺序排列。$\text{is\_rev}$ 取反后,逻辑顺序变为从右到左,等同于一次完整翻转。 后续插入依据新的 $\text{is\_rev}$ 走对应方向,维持不变式始终成立。
push_back(k);否则 push_front(k)。| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | $O(N)$ | 遍历 $N$ 次,每次 $O(1)$ 的插入和标记取反,最后 $O(N)$ 输出 |
| 空间复杂度 | $O(N)$ | 双端队列存储 $N$ 个元素 |
相比暴力做法的 $O(N^2)$,正解利用「整体翻转 → 懒标记」的关键转化,将物理翻转的代价完全消除,从 $O(N^2)$ 直降至 $O(N)$, 在 $N = 5 \times 10^5$ 下轻松通过。
#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;
}
| $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]$,与样例输出一致。
| $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]$,与样例输出一致。
| $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]$,与样例输出一致。
最容易遗漏的坑点:循环结束后 $\text{is\_rev}$ 可能为 $\text{true}$。这意味着 Deque 中元素在逻辑上是倒序的, 必须根据 $\text{is\_rev}$ 的状态决定从头输出还是从尾输出。若忽略这个判断直接正向输出,将得到错误答案。
懒标记 $\text{is\_rev}$ 表示当前序列是否处于翻转状态。插入操作和翻转操作依赖同一标记,顺序至关重要: 先基于当前标记插入 $k$,再根据 $S_k$ 翻转标记。若顺序颠倒,翻转操作的语义将偏移一位,导致结果错误。
给定初始值 $X$、目标值 $Y$ 和参数 $K$($K \ge 2$)。每次操作可以将当前数 $x$ 变为 $y$,要求满足以下两个条件之一:
求将 $X$ 变为 $Y$ 的最少操作次数。数据范围:$T \le 2 \times 10^5$,$0 \le X, Y \le 10^{18}$,$2 \le K \le 10^{18}$。题目保证在有限步内一定可以达成。
仔细观察两种操作在 $K$ 进制下的含义:
如果把每一个非负整数看作图上的一个节点,将上述转化关系看作边,会得到一个怎样的图?
在树上,两点之间的最短路径必然经过它们的最近公共祖先(LCA, Lowest Common Ancestor)。因此,答案即为:
$$\text{Ans} = \text{dist}(X, \text{LCA}) + \text{dist}(Y, \text{LCA})$$
其中 $\text{dist}(A, B)$ 表示树上节点 $A$ 到 $B$ 的路径长度(即深度差)。
以样例 $1$ 为例($X=11,\; Y=9,\; K=3$),状态树的局部如下:
0
/ | \
0 1 2
/|\
3 4 5
/|\
9 10 11
因为状态图是一棵无环连通图(树),树上两点之间的简单路径是唯一且最短的,因此通过寻找 LCA 计算出的距离必定是全局最优解,不存在其他"捷径"。该转化严格等价于原问题。
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间(单组) | $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$ 秒时限内绰绰有余。
#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;
}
下面逐一演算题目给定的 $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$ 步。
pathX.push_back(0) 确保根节点始终被加入,即使 $X$ 或 $Y$ 本身为 $0$。long long(C++ 中 $64$ 位有符号整数,上限约 $9.2 \times 10^{18}$)。给定一个极大的整数 $N$($1 \le N \lt 10^{500}$),求在区间 $[1, N]$ 中,满足以下三个条件中恰好一个的整数 $x$ 的数量,答案对 $998244353$ 取模:
题目保证了 $N$ 的十进制表示没有多余的前导零。
如果采用暴力做法,需要从 $1$ 遍历到 $N$ 逐一判断每个数是否满足「恰好一个条件」,时间复杂度高达 $O(N \log_{10} N)$。面对 $N \lt 10^{500}$ 的极大数据范围($N$ 最多有 $500$ 位),暴力做法完全不可行。
数位 DP 的核心思想是:从高位到低位逐位确定数字,同时用紧凑的状态压缩当前已构造前缀的信息,避免对每个数单独判断,从而将复杂度从 $O(N)$ 降到 $O(L \times \text{状态数})$($L$ 为 $N$ 的位数)。
我们需要在数位 DP 的每个状态中,能够判断当前已构造的数字前缀是否满足三个条件。为此设计以下状态维度:
条件 $1$($3$ 的倍数):一个数是 $3$ 的倍数,当且仅当其各位数字之和是 $3$ 的倍数。维护当前数字之和对 $3$ 取模的结果:
rem3 $\in \{0, 1, 2\}$,表示当前数字前缀之和 $\bmod 3$。条件 $2$(包含数字 $3$)与条件 $3$(恰好 $3$ 种数字):这两个条件都与「使用了哪些数字」有关。使用一个 $10$ 位的二进制数(bitmask)来记录数字 $0 \sim 9$ 的出现情况:
mask $\in \{0, 1, \dots, 1023\}$($10$ 位二进制,共 $2^{10} = 1024$ 种可能)。第 $i$ 位为 $1$ 表示数字 $i$ 出现在了该数中。mask 的第 $3$ 位为 $1$。mask 的二进制位中恰好有 $3$ 个 $1$(即 $\text{popcount}(\text{mask}) = 3$)。数位 DP 基础状态:
is_less:布尔值,表示当前前缀是否已经严格小于 $N$ 的前缀。若为 $0$,当前位最大只能填 $N$ 对应位置的数字;若为 $1$,当前位可填 $0 \sim 9$。is_started:布尔值,表示是否已经开始填入非零数字。这用于处理前导零——前导零不应被计入 mask。在填入当前位的数字 $d$ 时,转移规则如下:
is_started = 0 且 $d = 0$,说明当前填入的仍是前导零,mask 不变,nxt_started 保持 $0$。is_started = 1 或 $d \gt 0$,则进入「已开始」状态:nxt_started = 1,且 nxt_mask = mask | (1 << d)。rem3:$\text{nxt\_rem3} = (\text{rem3} + d) \bmod 3$。is_less:若当前已处于 is_less = 1,或在当前位填了小于该位上限的数字,则 nxt_less = 1。所有转移累加方案数,并时刻对 $998244353$ 取模。
处理完所有位后,遍历所有状态。只统计 is_started = 1 的状态(排除 $x = 0$ 的情况)。对于每个状态,判断三个条件的满足情况:
若 $\text{cond1} + \text{cond2} + \text{cond3} = 1$,则将该状态的方案数累加入答案。
dp[is_less][is_started][rem3][mask],其中 dp[0][0][0][0] = 1。max_digit。next_dp 全零。nxt_less、nxt_started、nxt_rem3、nxt_mask。next_dp 复制回 dp(滚动数组优化)。dp[is_less][1][rem3][mask],筛选恰好满足一个条件的状态,累加答案。| 项目 | 复杂度 | 说明 |
|---|---|---|
| 状态总数 | $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)$,常数极小。
#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;
}
| $\#$ | $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$ 为例,手动验证几个典型数字:
is_started 标志,数位 DP 会将短于 $L$ 位的数字前面的补位 $0$ 也算作「使用过的数字」,导致 mask 计算完全错误。例如数字 $12$(两位)在 $3$ 位的 DP 框架中会被错误地记录为使用了 $\{0, 1, 2\}$ 三种数字。必须通过 is_started 严格特判:只有开始填非零数字后,才将后续数字记入 mask。is_started = 1(即至少填入了一个非零数字),自动排除 $x = 0$ 的非法情况。未开始的空状态(全前导零)不会被计入。next_dp 的 $12288$ 个状态复制到 dp。如果只复制非零状态而遗漏了「从有变为无」的状态,会导致脏数据残留。本代码使用四重循环全量覆盖,确保安全。is_started 排除 → 滚动数组优化空间 → 最终统计「恰好一个」条件。
is_started = 0 时 mask 只能为 $0$,以及 is_less = 0 时某些 mask 组合不可能出现)。如果做进一步的状态压缩或可达性剪枝,常数可以再优化约 $2 \sim 4$ 倍。不过当前实现在 $2$ 秒时限内已绰绰有余。is_less、is_started)+ 多条件联合判定 + bitmask 状态压缩 + 滚动数组优化,属于 AtCoder ABC 的 E 题标准难度,对应 OI 标签体系中的提高级别。冰箱中有 $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$。
对于每次查询,遍历所有 $N$ 瓶饮料,逐位比对是否满足 $x_k \le (S_i)_k \le y_k$。单次查询时间复杂度 $O(N)$,总复杂度 $O(NQ)$。代入 $N, Q \le 3 \times 10^5$,最坏运算次数接近 $10^{11}$,必然超时。必须将单次查询优化到更低量级。
既然完整状态空间大小只有 $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$ 维超矩形内的元素之和。
设前缀和 $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$ 维前缀和。
有了 $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)$。
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 预处理(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 |
#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$ 瓶饮料:$\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$ |
共 $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$ 而非让容斥角点产生负数坐标。
sum 和答案变量 ans 必须使用 $64$ 位整数($\texttt{long long}$)。current_idx = current_idx * 10 + digit。这个顺序本质上定义了「前缀和」中各维度的偏序关系。tight 标记约束上下界,而此处通过高维前缀和 + 容斥一次性解决了所有可能的查询。给定长度为 $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$。
直接维护集合并求 $\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$。
既然单次 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)$$
注意到 $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):
求出对应参数下的 $\Delta f, \Delta g, \Delta h$,即可在 $O(\log M)$ 的时间内计算出任意一个区间对总 MEX 的贡献。
有了 $O(\log M)$ 算出单段区间贡献的工具,剩下的就是数据结构部分:
std::map<long long, long long> 存储线性的极大连续段 $[L, R]$。这一设计完美避开了环形区间分裂合并时的大量 Corner Cases。
std::map 维护频次 $freq$ 和线性极大连续段 $segs$,初始构建贡献和 $linear\_sum$。get_total_ans() 计算答案并输出。其中 calc_B_interval(u, v) 函数利用扩展类欧几里得算法,在 $O(\log M)$ 内计算单个区间 $[u, v]$ 对答案的贡献。
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间(预处理) | $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 与频次数组仅记录出现的元素 |
#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;
}
初始 $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$)。
| $\#$ | 修改 | $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$。
__int128_t 或等效的大整数类型。C++ 标准中 long long 不足以安全容纳所有中间结果。calc_B_interval 和 get_total_ans 两处都做特判。std::map 维护动态区间 → 环上首尾合并容斥 → $O((N+Q) \log M)$ 总复杂度。