利用 gcd(x,y)=gcd(x,y−x) 把区间 gcd 转到差分序列上:区间加塌缩成两个单点改,用树状数组求 A_l 的前缀和、线段树维护差分的区间 gcd,合并得答案。
OJ: roj
题目 ID: 3113
难度:提高+/省选-
标签:线段树树状数组差分gcdpython
创建: 2026-10-01 17:52
更新: 2026-10-10 11:30
形式化题目
给定长度为
C l r d:把都加上 ; Q l r:询问。
对每个询问输出一个整数。
数据范围:
思路
朴素做法为什么不行
最直接的做法是每次询问把区间逐个元素取
更根本的障碍在于:普通线段树擅长"单点改 + 区间可合并信息(和 / max)",
但本题是区间加 + 区间 gcd。加法懒标记对 gcd 没有传递规律——
突破口:把 gcd 搬到差分序列上
对相邻两项反复套用:设差分
也就是说,区间
而区间加操作在差分序列上会"塌缩":
(
| 需要的量 | 对应结构 | 操作 |
|---|---|---|
| 树状数组 | 维护 |
|
| 线段树 | 叶子是 |
合并答案
每次询问:
- 用树状数组求
; - 用线段树求
( 时为空,跳过); - 答案是
。
两个细节:
可为负, 与 都可能是负数, 对非零数取绝对值不影响结果, 所以对 显式取 ;线段树内部节点由 math.gcd合出,天然非负。是 的单位元:查询初值取 、线段树补零叶子都靠它成立, 的边界也自动正确。
用样例演示这个转化(初始
| 操作 | 原数列视角 | 差分视角 |
|---|---|---|
Q 1 5 |
||
C 1 5 1 |
||
Q 1 5 |
||
C 3 3 6 |
||
Q 2 4 |
两次修改每次都只动了差分序列上的至多两个点,查询则合并"一个前缀和 + 一段区间 gcd"。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 区间加 + 区间 gcd:差分后区间 gcd 变成单点改 + 区间 gcd,配 zkw 线段树与树状数组。
#include <cstdio>
#include <string>
typedef long long ll;
const ll MAXN = 500005;
const ll MAXSEG = 1 << 19;
ll n, m;
ll a[MAXN];
ll b[MAXN]; // 差分 b[i] = a[i] - a[i-1]
ll tree[2 * MAXSEG]; // zkw 线段树:叶子是 b[i],内部节点是子树 gcd
ll bit[MAXN]; // 树状数组维护 b 的前缀和
ll size; // 叶子层长度
ll gcd2(ll x, ll y) {
if (x < 0) x = -x; // 与 Python 的 math.gcd 一致:结果恒为非负
if (y < 0) y = -y;
while (y) {
ll t = x % y;
x = y;
y = t;
}
return x;
}
// 差分点 b[p] += d,同步更新线段树与树状数组
void update(ll p, ll d) {
ll i = size + p - 1;
tree[i] += d;
while (i > 1) { // 只有到根的一条链会变化
i >>= 1;
tree[i] = gcd2(tree[2 * i], tree[2 * i + 1]);
}
i = p;
while (i <= n) {
bit[i] += d;
i += i & -i;
}
}
// 差分 b[l..r](1-based)的 gcd
ll range_gcd(ll l, ll r) {
ll res = 0;
ll lo = size + l - 1, hi = size + r;
while (lo < hi) {
if (lo & 1) {
res = gcd2(res, tree[lo]);
lo++;
}
if (hi & 1) {
hi--;
res = gcd2(res, tree[hi]);
}
lo >>= 1;
hi >>= 1;
}
return res;
}
int main() {
if (scanf("%lld %lld", &n, &m) != 2) return 0; // 空输入安全返回
for (ll i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
}
for (ll i = 1; i <= n; i++) {
b[i] = a[i] - a[i - 1]; // 区间加 [l,r] 变成 b[l] += d、b[r+1] -= d
}
ll log = 1;
while ((1LL << log) < n) {
log++;
}
if (n <= 1) {
log = 1;
}
size = 1LL << log;
for (ll i = 1; i <= n; i++) {
tree[size + i - 1] = b[i];
}
for (ll i = size - 1; i >= 1; i--) {
tree[i] = gcd2(tree[2 * i], tree[2 * i + 1]);
}
// 树状数组 O(n) 建树
for (ll i = 1; i <= n; i++) {
bit[i] += b[i];
ll j = i + (i & -i);
if (j <= n) {
bit[j] += bit[i];
}
}
for (ll q = 0; q < m; q++) {
char op[4];
ll l, r;
scanf("%s %lld %lld", op, &l, &r);
if (op[0] == 'C') {
ll d;
scanf("%lld", &d);
update(l, d);
if (r < n) {
update(r + 1, -d);
}
} else {
// gcd(a[l..r]) = gcd(a[l], b[l+1..r]),a[l] 用前缀和求出
ll s = 0, i = l;
while (i) {
s += bit[i];
i -= i & -i;
}
ll res = s < 0 ? -s : s;
if (l < r) {
res = gcd2(res, range_gcd(l + 1, r));
}
printf("%lld\n", res);
}
}
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 17:52
# update_at: 2026-10-01 17:52
import sys
from math import gcd
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data))
m = int(next(data))
a = [0] + [int(next(data)) for _ in range(n)]
# 差分:b[i] = a[i] - a[i-1],区间加 [l,r] 变成 b[l] += d、b[r+1] -= d 两个单点改
b = [a[i] - a[i - 1] for i in range(1, n + 1)]
# zkw 线段树:叶子是 b[i],内部节点是子树 gcd;补零叶子不影响 gcd
log = max(1, (n - 1).bit_length())
size = 1 << log
tree = [0] * (2 * size)
tree[size:size + n] = b
for i in range(size - 1, 0, -1):
tree[i] = gcd(tree[2 * i], tree[2 * i + 1])
# 树状数组维护 b 的前缀和(O(n) 建树),a[l] = b[1] + ... + b[l]
bit = [0] * (n + 1)
for i in range(1, n + 1):
bit[i] += b[i - 1]
if (j := i + (i & -i)) <= n:
bit[j] += bit[i]
def update(p: int, d: int) -> None:
"""差分点 b[p] += d,同步更新线段树与树状数组。"""
i = size + p - 1
tree[i] += d
while i > 1: # 只有到根的一条链会变化,逐层重算 gcd
i >>= 1
tree[i] = gcd(tree[2 * i], tree[2 * i + 1])
i = p
while i <= n:
bit[i] += d
i += i & -i
def range_gcd(l: int, r: int) -> int:
"""差分 b[l..r](1-based)的 gcd,内部节点恒为非负。"""
res, lo, hi = 0, size + l - 1, size + r
while lo < hi:
if lo & 1:
res = gcd(res, tree[lo])
lo += 1
if hi & 1:
hi -= 1
res = gcd(res, tree[hi])
lo >>= 1
hi >>= 1
return res
out: list[str] = []
for _ in range(m):
op = next(data)
l = int(next(data))
r = int(next(data))
if op == b'C':
d = int(next(data))
update(l, d)
if r < n:
update(r + 1, -d)
else:
# gcd(a[l..r]) = gcd(a[l], b[l+1], ..., b[r]),a[l] 用前缀和求出
s, i = 0, l
while i:
s += bit[i]
i -= i & -i
res = abs(s)
if l < r:
res = gcd(res, range_gcd(l + 1, r))
out.append(str(res))
sys.stdout.write('\n'.join(out) + '\n')
if __name__ == "__main__":
solve()实现说明:
- zkw 线段树(非递归):
size取,叶子 平铺在 tree[size .. size+n-1],其余补(不影响 );建树自底向上。 单点修改先改叶子,再沿父链 tree[i>>1] = gcd(tree[2i], tree[2i+1])重算—— 只有到根的一条链会变化;区间查询用lo/hi双指针向上爬,把覆盖区间的节点并进res。 - 树状数组维护差分前缀和,用
tree[i] += delta[i]后向i + lowbit(i)累加的 写法建树,避免 次 插入。 - 读入用
sys.stdin.buffer.read().split()的扁平 token 流加pos指针顺序扫描, 操作符与b'C'直接比较,条指令的开销降到最低。
图示解析
下图画出样例最后一次查询 Q 2 4 时的两棵结构(
flowchart TB
subgraph BIT["树状数组:维护差分前缀和(求 A_l)"]
direction LR
B8["bit[8]<br/>= b1+..+b8"]:::path
B4["bit[4]<br/>= b1..b4"]:::path
B2["bit[2]"]:::dim
B1["bit[1]"]:::path
B8 --> B4 --> B2
B4 --> B1
end
subgraph ZKW["zkw 线段树:叶子 = 差分 b,节点 = 子树 gcd"]
direction TB
T1["[1..8]<br/>gcd=2"]:::dim
T2["[1..4]<br/>gcd=2"]:::dim
T3["[5..8]<br/>gcd=2"]:::dim
T4["[1..2]<br/>gcd=2"]:::dim
T5["[3..4]<br/>gcd=8"]:::path
T6["[5..6]"]:::dim
T7["[7..8]"]:::dim
T10["[1]"]:::dim
T11["[2]"]:::dim
T12["[3]<br/>b3=8"]:::path
T13["[4]<br/>b4=-4"]:::path
T1 --> T2 & T3
T2 --> T4 & T5
T4 --> T10 & T11
T5 --> T12 & T13
end
Q["Q 2 4:A_2 = bit 前缀和 = 4;区间 gcd(b3, b4) = gcd(8, -4) = 4;答案 gcd(4, 4) = 4"]
BIT --> Q
ZKW --> Q
classDef path fill:#ffe0b2,stroke:#e65100
classDef dim fill:#f5f5f5,stroke:#bbb,color:#999
- 树状数组侧:
,按 2 → 1的 lowbit 链把bit[2]、bit[1]累加起来。 - 线段树侧:
正好落在节点 [3..4]()内,一次合并完成; 若区间跨更多块,就在爬树过程中逐块并进 res。 - 单点修改(
C指令)在线段树上只重算叶子到根的一条链,树状数组上只改一条 lowbit 链, 这正是"区间加塌缩成单点改"换来的效率。
复杂度
- 预处理
:差分、线段树自底向上、树状数组 建树。 - 每次修改
C:至多两次单点更新,各。 - 每次询问
Q:一次前缀和+ 一次区间 (合并 个节点, 每次 计算 )→ 。 - 总时间
,空间 。 C++ 可轻松通过;Python 版实测 4 组测试数据均在 内完成。
总结
- 破题点是恒等式
:它把区间 gcd 搬到差分序列上, 让区间加塌缩成两个单点改,一个看似"懒标记失效"的难题就此拆成两个经典模型。 - 结构分工要记牢:
交给可合并的线段树,加法信息交给常数更小的树状数组(前缀和求 ); 合并公式 与边界 、 是易错点。 - "差分 + 单点维护 + 合并查询"是处理"区间加 + 不可懒传的区间信息"的通用套路, 同样适用于区间加 + 区间异或和、区间加 + 区间最大公约数变体等问题。