题单 · 习题解析

洛谷 P3373 【模板】线段树 2

★★★ 关键一步是**把两个懒标记读成一次仿射变换 f(x) = mul·x + add** ⇒ 「两笔账怎么叠」变成「两个仿射变换怎么复合」,一行算完:新 mul = a₂a₁、新 add = **a₂b₁** + b₂ —— 而 a₂·b₁ 正是所有人第一次都会漏掉的那句「乘的时候旧的加法标记也要跟着乘」,「先乘后加」也只是这个式子的推论,**不用背**;⚠⚠ 三个错法**官方样例一个都没挡住**(本书第三次拿到这个极端)—— int 版是因为**样例的模数只有 38**(中间值 38² = 1444,而真实数据 m = 571373 ⇒ m² = 3.26×10¹¹,是 int 上限的 152 倍);★★★ 而抓获率被「区间形状」这个旋钮推成**单峰**:全整段 **0** → 短区间 8/20 → 随机 57/87 ⇒ ⚠⚠ 我的草稿写「全整段 ⇒ 标记堆在根上 ⇒ 抓满」被实测打回,真因一个数就说清了:**那一档 pushdown 执行了 0 次**(全覆盖 ⇒ 不往下走 ⇒ 标记错成什么样都没人读);★ 「只有加法」「乘数恒为 1」两档的 0 都能证,正好当自检;★ 暴力值 **70 分**(题面写好了),正解顶格 126 ms / 余量 8 倍;⚠ 而 [P3372](/sol/p3372/) 那条「两个树状数组也能做」的路在这道题上**没了** —— 差分靠「区间加 = 两个单点改」,而乘法没有这个性质

原题:洛谷 P3373出自 第 39 章 线段树入门 的题单题面本地存档:2026-09-01
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P3373,日期见页头。两边不一致时信原站。

题目描述

如题,已知一个数列 a,你需要进行下面三种操作:

  • 将某区间每一个数乘上 x
  • 将某区间每一个数加上 x
  • 求出某区间每一个数的和。

输入格式

第一行包含三个整数 nqm,分别表示该数列数字的个数、操作的总个数和模数

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值 aᵢ

接下来 q 行每行包含若干个整数,表示一个操作,具体如下:

  • 操作 1,格式 1 x y k:将区间 [x, y] 内每个数乘上 k
  • 操作 2,格式 2 x y k:将区间 [x, y] 内每个数加上 k
  • 操作 3,格式 3 x y:输出区间 [x, y] 内每个数的和对 m 取模所得的结果。

输出格式

输出包含若干行整数,即为所有操作 3 的结果。

数据范围

对于 30% 的数据:n ≤ 8q ≤ 10

对于 70% 的数据:n ≤ 10³q ≤ 10⁴

对于 100% 的数据:1 ≤ n ≤ 10⁵1 ≤ q ≤ 10⁵1 ≤ aᵢ, k ≤ 10⁴

除样例外,m = 571373(数据已经过加强)

时限 1 秒,内存 125 MB。

输入输出样例

输入

5 5 38
1 5 4 2 3
2 1 4 1
3 2 5
1 2 4 2
2 3 5 5
3 1 4

输出

17
2

★ 样例说明(转录自原站那张表,见下图):m = 38,初始 1 5 4 2 32 1 4 1[1,4] 各加 1 ⇒ 2 6 5 3 33 2 56+5+3+3 = 171 2 4 2[2,4] 各乘 2 ⇒ 2 12 10 6 32 3 5 5[3,5] 各加 5 ⇒ 2 12 15 11 83 1 42+12+15+11 = 40,而 40 mod 38 = 2

P3373 样例说明:五次操作、每一步之后的数列,以及两个输出

1第 ① 版:三种操作全都一个一个做 —— ★ 值 70 分

p3373Brute.cpp第 ① 版:每次操作 O(区间长) —— ★ 70 分,也是参照物
// P3373 第 ① 版:三种操作全都一个一个做
//
// ★ 题面把它的分数写好了:「30% 的数据 n ≤ 8,q ≤ 10」「70% 的数据 n ≤ 10³,q ≤ 10⁴」
// ⇒ 70% 那一档最坏 10³ × 10⁴ = 10⁷ 次 ⇒ **稳拿 70 分**(本机毫秒级)。
// 顶格 n = q = 10⁵ 是 10¹⁰ ⇒ 0 分。
// ⚠ 它同时是参照物。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long n, q, mod;
if (!(cin >> n >> q >> mod)) return 0;
vector<long long> a(n + 1);
for (long long i = 1; i <= n; i++) { cin >> a[i]; a[i] %= mod; }
string out;
for (long long i = 0; i < q; i++) {
int op;
long long x, y, k;
cin >> op >> x >> y;
if (op == 3) {
long long s = 0;
for (long long j = x; j <= y; j++) s = (s + a[j]) % mod;
out += to_string(s);
out += '\n';
} else {
cin >> k;
k %= mod;
for (long long j = x; j <= y; j++)
a[j] = (op == 1) ? a[j] * k % mod : (a[j] + k) % mod;
}
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面又一次把暴力的分数写好了:70% 那一档 n ≤ 10³q ≤ 10⁴ ⇒ 最坏 10⁷ 次, 本机实测 11 毫秒(碰了 332 万个格子)⇒ 稳拿 70 分。 顶格 n = q = 10⁵10¹⁰ ⇒ 0 分。

2★★★ 关键一步:把两个懒标记读成「一个仿射变换」

★ 别背「先乘后加」—— 它是一个式子的推论

一个节点现在欠儿子两笔账:先乘 mul,再加 add。把它写成一个函数:

f(x) = mul · x + add

这就是一次仿射变换。于是「两笔账怎么叠」就变成「两个仿射变换怎么复合」,一行算完:

先做 f₁(x) = a₁x + b₁,再做 f₂(x) = a₂x + b₂
⇒ f₂(f₁(x)) = a₂(a₁x + b₁) + b₂ = (a₂a₁)·x + (a₂b₁ + b₂)

新的 mul = a₂·a₁,新的 add = a₂·b₁ + b₂

★★ 请盯着 a₂ · b₁ 这一项 —— 它就是所有人第一次都会漏掉的那句 「乘的时候,原来欠着的加法标记也要跟着乘」。 ★★ 而「先乘后加」也不是要背的规矩:标记的含义一旦定成 f(x) = mul·x + add, 「这一段的和该怎么变」就只能写成 sm ← M·sm + A·len

两个标记的所有细节,都是这一个式子的分量。

p3373.cpp正解:双标记 = 仿射变换的复合,O((n + q) log n)
// P3373 正解 —— 线段树,两个懒标记(乘 + 加)
//
// ============ ★★★ 关键一步:把两个标记读成「一个仿射变换」============
//
// 一个节点欠儿子的账,现在是**两笔**:先乘 mul,再加 add。
// 把它写成一个函数:**f(x) = mul · x + add** —— 这就是一次**仿射变换**。
//
// ★ 于是「两笔账怎么叠」这件事,就变成了「两个仿射变换怎么复合」,一行算完:
//
// 先做 f₁(x) = a₁x + b₁,再做 f₂(x) = a₂x + b₂
// ⇒ f₂(f₁(x)) = a₂(a₁x + b₁) + b₂ = (a₂a₁)·x + (a₂b₁ + b₂)
//
// ⇒ **新的 mul = a₂·a₁,新的 add = a₂·b₁ + b₂。**
// ★★ 请注意 `a₂ · b₁` 这一项 —— 它就是所有人第一次都会漏掉的那句
// 「**乘的时候,原来欠着的加法标记也要跟着乘**」(见 p3373NoMul.cpp)。
// ★★ 而「先乘后加」这个约定,也不是背下来的规矩,它就是上面那个式子的写法:
// 标记的含义一旦定成 f(x) = mul·x + add,下推的顺序就**只能**是先乘后加
// (见 p3373Order.cpp)。
//
// ⇒ 一句话:**两个标记的所有细节,都是「仿射变换复合」这一个式子的分量。**
//
// ============ ⚠ 要不要 long long:一句算术 ============
//
// 题面「除样例外 m = 571373」。节点的和 < m,标记 < m
// ⇒ 中间值 sm[o] · mul 最大约 m² = 571373² ≈ **3.26 × 10¹¹** > 2³¹
// ⇒ **int 必炸,long long 够用(余量 2800 万倍)。**
// ⚠ 而**样例的 m 只有 38** ⇒ 中间值最大 1444 ⇒ **样例根本挡不住 int 版**(见 p3373Int.cpp)。
//
// 复杂度:每次操作 O(log n)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
long long sm[MAXN * 4], mul[MAXN * 4], add_[MAXN * 4];
long long a[MAXN];
long long n, q, mod;
inline void pushup(int o) { sm[o] = (sm[o * 2] + sm[o * 2 + 1]) % mod; }
/** 给节点 o(段长 len)施加一次仿射变换 x -> M·x + A */
inline void applyAff(int o, long long len, long long M, long long A) {
sm[o] = (sm[o] * M + A % mod * len) % mod;
mul[o] = mul[o] * M % mod;
add_[o] = (add_[o] * M + A) % mod; // ★★ 就是这个 add_[o] * M:旧的加法标记也要跟着乘
}
inline void pushdown(int o, int l, int r) {
if (mul[o] == 1 && add_[o] == 0) return;
int mid = (l + r) / 2;
applyAff(o * 2, mid - l + 1, mul[o], add_[o]);
applyAff(o * 2 + 1, r - mid, mul[o], add_[o]);
mul[o] = 1; add_[o] = 0; // 销账:恒等变换
}
void build(int o, int l, int r) {
mul[o] = 1; add_[o] = 0;
if (l == r) { sm[o] = a[l] % mod; return; }
int mid = (l + r) / 2;
build(o * 2, l, mid);
build(o * 2 + 1, mid + 1, r);
pushup(o);
}
void update(int o, int l, int r, int ql, int qr, long long M, long long A) {
if (ql <= l && r <= qr) { applyAff(o, r - l + 1, M, A); return; }
pushdown(o, l, r);
int mid = (l + r) / 2;
if (ql <= mid) update(o * 2, l, mid, ql, qr, M, A);
if (qr > mid) update(o * 2 + 1, mid + 1, r, ql, qr, M, A);
pushup(o);
}
long long query(int o, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return sm[o];
pushdown(o, l, r);
int mid = (l + r) / 2;
long long s = 0;
if (ql <= mid) s += query(o * 2, l, mid, ql, qr);
if (qr > mid) s += query(o * 2 + 1, mid + 1, r, ql, qr);
return s % mod;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> q >> mod)) return 0;
for (long long i = 1; i <= n; i++) cin >> a[i];
build(1, 1, (int)n);
string out;
for (long long i = 0; i < q; i++) {
int op;
long long x, y, k;
cin >> op >> x >> y;
if (op == 3) { out += to_string(query(1, 1, (int)n, (int)x, (int)y)); out += '\n'; }
else {
cin >> k;
k %= mod;
if (op == 1) update(1, 1, (int)n, (int)x, (int)y, k, 0); // 乘 k:x -> k·x + 0
else update(1, 1, (int)n, (int)x, (int)y, 1, k); // 加 k:x -> 1·x + k
}
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3⚠⚠ 三个错法 —— 而官方样例一个都没挡住

p3373NoMul.cpp✗ 之一:乘的时候忘了把旧的加法标记也乘上(漏掉 a₂·b₁)
p3373Order.cpp✗ 之二:sm 按「先加后乘」算 —— 顺序反了
p3373Int.cpp✗ 之三:树里用 int
★★ 三个「运行 ▶」你都可以点一下 —— 三份都打出 17 和 2

这是本书第三次拿到「样例把错法全部放过」这个极端 (前两次是第 32 章 P3371第 27 章 P3478)。 而这一次每一个「放过」都说得出具体原因:

  • 错法三(int:样例的 m 只有 38 ⇒ 中间值 sm · mul 最大 38² = 1444, int 绰绰有余。★ 而真实数据 m = 571373m² = 3.26 × 10¹¹,是 int 上限的 152 倍。 ⇒ 样例的模数比真实数据小四个数量级。
  • 错法一、二:样例里加法和乘法虽然都有,但它们没在同一个节点上碰头n = 5、区间又短,标记还没叠起来就被推下去了)。

4★★★ 而这三个错法的抓获率,被同一个旋钮推向了三个方向

p3373Gen.cpp(八个档位)★ 每一档都是照着「某个错法靠什么现形」造的
// P3373 的生成器:./p3373Gen 种子 [档位]
//
// ★ 动笔之前先写清楚「每个版本靠什么现形」:
// · p3373NoMul(乘的时候没乘旧的加法标记)→ 要**同一个节点上先欠加法、再来一次 M ≠ 1 的乘法**。
// ⇒ 档 2(只有加法)和档 3(乘数恒为 1)都是**能证的 0**。
// · p3373Order(sm 先加后乘)→ 要**同一段上 A 和 M 都不平凡** ⇒ 同样被档 2 / 档 3 挡死。
// · p3373Int(树里用 int)→ 要中间值 sm·mul 越过 2³¹ ⇒ **模数说了算**:
// ⚠ 档 0 用样例那个 m = 38 ⇒ 中间值最大 1444 ⇒ **精确的 0**;档 1 起换成题面的 571373。
//
// 档位:
// 0 ★ 顺手写的样子:n, q ∈ [6,12],**模数照抄样例的 38** ⇒ int 版精确的 0
// 1 ★ 照题面:模数 571373 ⇒ int 版现形
// 2 ★★ 只有「加」和「查」,没有乘法 ⇒ 两个标记错法都是能证的 0
// 3 ★★ 有乘法,但乘数恒为 1 ⇒ 同上(自检:0 不是因为代码没跑)
// 4 「70% 的数据」那一档:n = 10³、q = 10⁴(量暴力真实的秒数)
// 5 顶格 n = q = 10⁵,三种操作各三分之一
// 6 ⚠⚠ 区间**恒为整段 [1, n]** —— 我本来以为「两笔账全堆在根上 ⇒ 抓获率拉满」,
// **实测是精确的 0,而且能证**:所有操作都全覆盖 ⇒ pushdown 一次都不执行
// ⇒ 标记错成什么样都没人读得到(查询直接拿根上的 sm)。
// 7 ★★ 反过来:区间长度 ≤ 3 —— 逼着每次都往下走 ⇒ pushdown 密集
//
// ⚠ rng() 一律先落到具名变量再传参([第 24 章 P1776](/sol/p1776/) 那一跤)。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int mode = argc > 2 ? atoi(argv[2]) : 0;
mt19937 rng(seed * 1000003u + 20260901u);
int n, q;
if (mode == 4) { n = 1000; q = 10000; }
else if (mode == 5) { n = 100000; q = 100000; }
else { n = 6 + (int)(rng() % 7u); q = 6 + (int)(rng() % 7u); }
long long mod = (mode == 0) ? 38 : 571373;
printf("%d %d %lld\n", n, q, mod);
for (int i = 1; i <= n; i++) {
int v = 1 + (int)(rng() % 10000u);
printf("%d%c", v, i == n ? '\n' : ' ');
}
for (int t = 0; t < q; t++) {
int x = 1 + (int)(rng() % (unsigned)n);
int y = 1 + (int)(rng() % (unsigned)n);
if (x > y) swap(x, y);
if (mode == 6) { x = 1; y = n; }
if (mode == 7) { int len = 1 + (int)(rng() % 3u); y = min(n, x + len - 1); }
int op;
if (mode == 2) op = (rng() % 2u == 0) ? 2 : 3; // 只有加和查
else op = 1 + (int)(rng() % 3u);
if (op == 3) { printf("3 %d %d\n", x, y); continue; }
int k;
if (mode == 3 && op == 1) k = 1; // 乘数恒为 1
else k = 1 + (int)(rng() % 10000u);
printf("%d %d %d %d\n", op, x, y, k);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★ 每档 300 轮,配上「pushdown 到底执行了几次」这把尺子
档位 pushdown 执行次数 错法一被抓 错法二被抓 错法三(int)被抓
0 顺手写的(模数照抄样例的 38 1378 52 78 精确的 0
★ 1 照题面(模数 571373) 1426 57 87 76
2 只有「加」和「查」,没有乘法 1213 0 0 0
3 有乘法,但乘数恒为 1 849 0 0 0
⚠⚠ 6 区间恒为整段 [1, n] 0 0 0 84
7 区间长度 ≤ 3 437 8 20 25

★★★ 第 ⑤ 行是我的草稿被实测打回来的一处:我本来以为 「把区间拉满 ⇒ 两笔账全堆在根上 ⇒ 抓获率拉满」。实测是精确的 0,而且能证 —— 最后一列那把尺子直接给出了理由:pushdown 执行了 0 次。 所有操作都「全覆盖」⇒ 一次都不用往下走 ⇒ 标记错成什么样都没人读得到 (查询直接拿根上的 sm,而 sm 那一行两版都是对的)。 ⇒ 这和第 37 章 P1801 那次是同一个形状:那个分支根本没被执行。

★★★ 而把两头连起来看,这是一条单峰曲线: 0(全整段)→ 8 / 20(短区间)→ 57 / 87(随机区间)—— 两头都低,而峰顶正好是最顺手的那一档。 ⇒ 本书第四次量到单峰(前三次是第 13 章 P1596第 20 章 P1048第 30 章 B3625), ⚠ 但这一次的方向和常见的相反:以前是「顺手那一档最差」,这次是顺手那一档最好 (和第 36 章那条对上了:极端档往往结构太规整,反而把 bug 喂对了)。

★ 第 ③ ④ 行那两个 0 都是能证的自检:没有乘法 ⇒ a₂ 恒为 1 ⇒ 漏掉的 a₂·b₁ 就是 b₁ 本身, 一分不差;乘数恒为 1 同理。⇒ 它们证明那两段代码确实是活的, 只是这两档在结构上问不出那个问题。

p3373Count.cpp本页所有数字的出处(含那把「pushdown 执行了几次」的尺子)
// P3373 的度量程序:./p3373Count csv (本页的数字都出自它)
//
// down : ★★★ 每一档里 **pushdown 真正执行了多少次** ——
// 这一个数就把「档 6(全整段)为什么是精确的 0」交代干净了:
// 所有操作都全覆盖 ⇒ 一次都不往下走 ⇒ 标记错成什么样都没人读得到。
// line : ★ int 版那条线是算术:中间值 sm·mul 最大约 m²。
// 样例的 m = 38 ⇒ 1444(int 绰绰有余);题面的 m = 571373 ⇒ 3.26×10¹¹(int 的 152 倍)。
// ms : 70% 那一档(n=10³、q=10⁴)和顶格(n=q=10⁵)上暴力 / 线段树的毫秒。
//
// ⚠ 秒表一律用 steady_clock 在进程内量。
// ⚠ 这里**复刻**了 p3373Gen.cpp 的档位逻辑(同一个 mt19937、同一个种子公式)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
struct Op { int type, l, r; long long k; };
struct Data { int n, q; long long mod; vector<long long> a; vector<Op> ops; };
static Data gen(unsigned seed, int mode) {
mt19937 rng(seed * 1000003u + 20260901u);
Data d;
int n, q;
if (mode == 4) { n = 1000; q = 10000; }
else if (mode == 5) { n = 100000; q = 100000; }
else { n = 6 + (int)(rng() % 7u); q = 6 + (int)(rng() % 7u); }
d.mod = (mode == 0) ? 38 : 571373;
d.n = n; d.q = q; d.a.assign(n + 1, 0);
for (int i = 1; i <= n; i++) d.a[i] = 1 + (int)(rng() % 10000u);
for (int t = 0; t < q; t++) {
int x = 1 + (int)(rng() % (unsigned)n);
int y = 1 + (int)(rng() % (unsigned)n);
if (x > y) swap(x, y);
if (mode == 6) { x = 1; y = n; }
if (mode == 7) { int len = 1 + (int)(rng() % 3u); y = min(n, x + len - 1); }
int op;
if (mode == 2) op = (rng() % 2u == 0) ? 2 : 3;
else op = 1 + (int)(rng() % 3u);
if (op == 3) { d.ops.push_back({3, x, y, 0}); continue; }
int k;
if (mode == 3 && op == 1) k = 1;
else k = 1 + (int)(rng() % 10000u);
d.ops.push_back({op, x, y, k});
}
return d;
}
static long long MOD, downCnt, nodeCnt;
static vector<long long> sm, mul_, add_;
static void applyAff(int o, long long len, long long M, long long A) {
sm[o] = (sm[o] * M + A % MOD * len) % MOD;
mul_[o] = mul_[o] * M % MOD;
add_[o] = (add_[o] * M + A) % MOD;
}
static void down(int o, int l, int r) {
if (mul_[o] == 1 && add_[o] == 0) return;
downCnt++;
int mid = (l + r) / 2;
applyAff(o * 2, mid - l + 1, mul_[o], add_[o]);
applyAff(o * 2 + 1, r - mid, mul_[o], add_[o]);
mul_[o] = 1; add_[o] = 0;
}
static void build(int o, int l, int r, const vector<long long>& a) {
mul_[o] = 1; add_[o] = 0;
if (l == r) { sm[o] = a[l] % MOD; return; }
int mid = (l + r) / 2;
build(o * 2, l, mid, a); build(o * 2 + 1, mid + 1, r, a);
sm[o] = (sm[o * 2] + sm[o * 2 + 1]) % MOD;
}
static void upd(int o, int l, int r, int ql, int qr, long long M, long long A) {
nodeCnt++;
if (ql <= l && r <= qr) { applyAff(o, r - l + 1, M, A); return; }
down(o, l, r);
int mid = (l + r) / 2;
if (ql <= mid) upd(o * 2, l, mid, ql, qr, M, A);
if (qr > mid) upd(o * 2 + 1, mid + 1, r, ql, qr, M, A);
sm[o] = (sm[o * 2] + sm[o * 2 + 1]) % MOD;
}
static long long qry(int o, int l, int r, int ql, int qr) {
nodeCnt++;
if (ql <= l && r <= qr) return sm[o];
down(o, l, r);
int mid = (l + r) / 2;
long long s = 0;
if (ql <= mid) s += qry(o * 2, l, mid, ql, qr);
if (qr > mid) s += qry(o * 2 + 1, mid + 1, r, ql, qr);
return s % MOD;
}
static vector<long long> segRun(const Data& d) {
MOD = d.mod; sm.assign((size_t)d.n * 4 + 4, 0); mul_.assign((size_t)d.n * 4 + 4, 1);
add_.assign((size_t)d.n * 4 + 4, 0);
build(1, 1, d.n, d.a);
vector<long long> out;
for (const Op& o : d.ops) {
if (o.type == 3) out.push_back(qry(1, 1, d.n, o.l, o.r));
else if (o.type == 1) upd(1, 1, d.n, o.l, o.r, o.k % d.mod, 0);
else upd(1, 1, d.n, o.l, o.r, 1, o.k % d.mod);
}
return out;
}
static vector<long long> bruteRun(const Data& d, long long& touched) {
vector<long long> a(d.a), out;
for (int i = 1; i <= d.n; i++) a[i] %= d.mod;
touched = 0;
for (const Op& o : d.ops) {
if (o.type == 3) { long long s = 0; for (int j = o.l; j <= o.r; j++) { s = (s + a[j]) % d.mod; touched++; } out.push_back(s); }
else for (int j = o.l; j <= o.r; j++) { a[j] = (o.type == 1) ? a[j] * (o.k % d.mod) % d.mod : (a[j] + o.k) % d.mod; touched++; }
}
return out;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
const int MODES[6] = {0, 1, 2, 3, 6, 7};
long long downs[6] = {0}, nodes[6] = {0};
for (int i = 0; i < 6; i++) {
for (int s = 1; s <= 300; s++) {
Data d = gen(s, MODES[i]);
downCnt = 0; nodeCnt = 0;
segRun(d);
downs[i] += downCnt; nodes[i] += nodeCnt;
}
}
Data d70 = gen(1, 4);
long long touched = 0;
auto t0 = steady_clock::now(); vector<long long> ob = bruteRun(d70, touched);
double msB = duration<double, milli>(steady_clock::now() - t0).count();
t0 = steady_clock::now(); vector<long long> os = segRun(d70);
double msS = duration<double, milli>(steady_clock::now() - t0).count();
bool same70 = (ob == os);
Data dTop = gen(1, 5);
t0 = steady_clock::now(); segRun(dTop);
double msTop = duration<double, milli>(steady_clock::now() - t0).count();
long long downTop = downCnt;
long long m1 = 38LL * 38, m2 = 571373LL * 571373;
if (csv) {
for (int i = 0; i < 6; i++) printf("down%d,%lld\nnode%d,%lld\n", MODES[i], downs[i], MODES[i], nodes[i]);
printf("sq38,%lld\nsq571373,%lld\n", m1, m2);
printf("intTimes,%d\n", (int)(m2 / 2147483647LL));
printf("ms70b,%.0f\nms70s,%.0f\ntouched70,%lld\n", msB, msS, touched);
printf("msTop,%.0f\ndownTop,%lld\n", msTop, downTop);
printf("same70,%d\n", same70 ? 1 : 0);
return 0;
}
printf("① 每一档 300 轮里,pushdown 真正执行了多少次(这一个数解释了两个「精确的 0」):\n\n");
const char* NM[6] = {"0 顺手(模数 38)", "1 照题面(模数 571373)", "2 只有加和查",
"3 乘数恒为 1", "★ 6 区间恒为整段 [1,n]", "7 区间长度 ≤ 3"};
for (int i = 0; i < 6; i++)
printf(" %-26s pushdown %9lld 次 | 递归到的节点 %10lld 个\n", NM[i], downs[i], nodes[i]);
printf("\n② int 那条线(中间值 sm · mul 最大约 m²):\n");
printf(" 样例的 m = 38 ⇒ %lld (int 绰绰有余 ⇒ 样例挡不住 int 版)\n", m1);
printf(" 题面的 m = 571373 ⇒ %lld (是 int 上限的 %lld 倍 ⇒ 必炸)\n", m2, m2 / 2147483647LL);
printf("\n③ 秒表:\n");
printf(" 70%% 那一档 n=10³ q=10⁴ : 暴力 %.0f ms(碰 %lld 格)|线段树 %.0f ms,答案一致:%s\n",
msB, touched, msS, same70 ? "是" : "★ 否");
printf(" 顶格 n=q=10⁵ : 线段树 %.0f ms(pushdown %lld 次)\n", msTop, downTop);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

5★ 哪一版就已经能过了;以及树状数组为什么在这道题上出局

正解顶格 126 毫秒,余量 8 倍
版本 顶格 n = q = 10⁵ 交上去
① 暴力 约 10 秒 70 分
★ ② 线段树 + 双标记 126 mspushdown 226 万次) AC,余量 8 倍

⚠ 而隔壁 P3372 里那条「两个树状数组也能做」的路,在这道题上没了: 差分那一步靠的是「区间加 = 两个单点改」,而乘法没有这个性质 (一段乘 k,差分数组里每一项都得跟着变)。

⇒ ★★ 这就是第 39 章第 13 步那句话最干净的证据: 不是线段树更快,是它能干的事多。 同一章五道题,P3372 上树状数组更短更快,到这道题它连门都进不来。