等差数列区间加可拆系数用双 Fenwick 维护差分,也可用线段树等差数列懒标记,两者均 O(log n)。
OJ: luogu
题目 ID: P1438
难度:普及+/提高-
标签:树状数组差分等差数列线段树懒标记
日期: 2026-07-16 23:59
形式化题目
有一个长度为
- 对区间
内的每个位置 加上 (一个首项 、公差 的等差数列); - 询问位置
的当前值 。
要求按顺序处理全部操作并输出每次询问的答案。
暴力
先看一个可以直接验证想法的朴素解:
/**
* 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-08-12 22:03
* update_at: 2026-08-12 22:03
*/
// brute.cpp:小数据暴力解,直接按题意逐项加等差数列,用来理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m;
long long a[MAXN]; // a[i] 表示当前位置的当前值
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
while (m--) {
int opt;
cin >> opt;
if (opt == 1) {
int l, r;
long long K, D;
cin >> l >> r >> K >> D;
// 区间加等差数列:暴力逐项加。
for (int i = l; i <= r; i++) {
a[i] += K + (i - l) * D;
}
} else {
int p;
cin >> p;
cout << a[p] << '\n';
}
}
return 0;
}brute.cpp 直接维护数组:操作 1 逐项加等差数列,操作 2 输出当前位置,单次操作
思路
本题有两种 main.cpp:
- 解法一:把等差数列拆成"常数系数 + 下标系数",两个差分数组退化成四次端点修改,前缀和由两个 Fenwick 维护——代码最简单、常数最小。
- 解法二:直接在线段树上打"等差数列"懒标记,整段命中的节点
结算区间和——概念更通用,可扩展成区间查询。
解法一:双树状数组(差分)
思路
关键观察是拆系数:第
是一个"常数部分
用两个 Fenwick:c_diff 维护常数系数的差分,x_diff 维护下标系数的差分。操作 1 只做四次端点单点加;操作 2 输出
数学视角:为什么差分 + 树状数组可行
- 查询信息构成幺半群:单点值由差分前缀和恢复,前缀和用
合并,构成交换幺半群 。 - 区间加常数是摘要上的自同态:差分数组的单点加
等价于"所有位置 的前缀和同时加 ": ,且多个加法可合并成一个和。这就是树状数组可以把修改压缩到 个节点、查询沿 lowbit 累加的原因。
以样例为例,操作 1 2 4 1 2(
| 位置 |
2 | 3 | 4 |
|---|---|---|---|
| 常数系数 |
-3 | -3 | -3 |
| 下标系数 |
4 | 6 | 8 |
| 增量合计 | 1 | 3 | 5 |
| 修改后 |
3 | 6 | 9 |
观察表中"增量合计"一行:它正是
代码
/**
* 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-08-12 22:03
* update_at: 2026-08-12 22:03
*/
#include <bits/stdc++.h>
using namespace std;
// 仿照 rbook 模板 fenwick:单点加、前缀和,下标从 1 开始。
template <typename T>
struct Fenwick {
int n = 0;
vector<T> tree;
Fenwick(int n = 0) {
init(n);
}
void init(int size) {
n = size;
tree.assign(n + 1, 0);
}
static int lowbit(int x) {
return x & -x;
}
// 给差分数组的一个位置增加 value。
void add(int pos, T value) {
for (int i = pos; i <= n; i += lowbit(i)) {
tree[i] += value;
}
}
// 求差分数组 [1, pos] 的前缀和。
T prefix_sum(int pos) const {
T answer = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
answer += tree[i];
}
return answer;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<long long> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
Fenwick<long long> c_diff(n); // 常数系数 (K - l*D) 的差分数组
Fenwick<long long> x_diff(n); // 下标系数 D 的差分数组
while (m--) {
int opt;
cin >> opt;
if (opt == 1) {
int l, r;
long long K, D;
cin >> l >> r >> K >> D;
// 第 i 项增加 (K - l*D) + i*D,两个系数都在 [l,r] 上加常数,
// 于是差分数组只需在 l 处加、r+1 处减。
long long c = K - l * D;
c_diff.add(l, c);
c_diff.add(r + 1, -c);
x_diff.add(l, D);
x_diff.add(r + 1, -D);
} else {
int p;
cin >> p;
// a[p] = 初始值 + 常数系数前缀和 + p * 下标系数前缀和。
long long ans = a[p] + c_diff.prefix_sum(p) + p * x_diff.prefix_sum(p);
cout << ans << '\n';
}
}
return 0;
}复杂度
- 时间:单次操作
,总 。 - 空间:两个 Fenwick 加初始数组,
。
解法二:线段树(等差数列懒标记)
思路
线段树不拆系数,直接把"待加的等差数列"作为懒标记:节点除了存区间和 sum,还存两个懒标记 first(该区间最左端位置待加的值)与 diff(公差)。整段命中时
下传时右儿子的首项要平移:左端从 sum 就是当前位置的当前值。
与解法一对比:解法二不需要拆系数公式,任何"等差数列区间加"都能直接打标记(包括区间加普通常数 long long 懒标记,常数比双 Fenwick 略大。
代码
/**
* 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-08-13 08:46
* update_at: 2026-08-13 08:46
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005;
ll a[MAXN]; // 初始数列
// 仿照 rbook 模板 segtree-range-assign 的 pull/apply/push 结构,
// 懒标记从「区间赋值」改成「待加等差数列」。
struct SegmentTreeAP {
int n = 0;
vector<ll> sum; // sum[p]:节点 p 区间的和
vector<ll> first; // 懒标记:该区间最左端位置待加的值
vector<ll> diff; // 懒标记:该区间待加的公差
SegmentTreeAP(int n = 0) {
init(n);
}
void init(int size) {
n = size;
sum.assign(n * 4 + 5, 0);
first.assign(n * 4 + 5, 0);
diff.assign(n * 4 + 5, 0);
}
void pull(int p) {
sum[p] = sum[p << 1] + sum[p << 1 | 1];
}
// 节点 p 的整段区间 [l, r] 加上首项 f、公差 d 的等差数列。
// 区间和增加 len*f + d*len*(len-1)/2(首项到末项求和)。
void apply(int p, int l, int r, ll f, ll d) {
ll len = r - l + 1;
sum[p] += len * f + d * len * (len - 1) / 2;
first[p] += f;
diff[p] += d;
}
// 把节点 p 的懒标记下传给两个儿子。
void push(int p, int l, int r) {
if (first[p] == 0 && diff[p] == 0)
return;
int mid = (l + r) >> 1;
apply(p << 1, l, mid, first[p], diff[p]);
// 右儿子左端位置是 mid+1,首项要平移 (mid + 1 - l) * diff。
apply(p << 1 | 1, mid + 1, r, first[p] + (mid + 1 - l) * diff[p], diff[p]);
first[p] = 0;
diff[p] = 0;
}
void build(int l, int r, int p = 1) {
if (l == r) {
sum[p] = a[l];
return;
}
int mid = (l + r) >> 1;
build(l, mid, p << 1);
build(mid + 1, r, p << 1 | 1);
pull(p);
}
// 区间 [ql, qr] 加上首项 K、公差 D 的等差数列:
// 位置 i 增加 K + (i - ql) * D。
void add_ap(int ql, int qr, ll K, ll D, int l, int r, int p = 1) {
if (ql <= l && r <= qr) {
apply(p, l, r, K + (l - ql) * D, D);
return;
}
push(p, l, r);
int mid = (l + r) >> 1;
if (ql <= mid)
add_ap(ql, qr, K, D, l, mid, p << 1);
if (qr > mid)
add_ap(ql, qr, K, D, mid + 1, r, p << 1 | 1);
pull(p);
}
// 单点查询位置 pos 的当前值。
ll query(int pos, int l, int r, int p = 1) {
if (l == r)
return sum[p];
push(p, l, r);
int mid = (l + r) >> 1;
if (pos <= mid)
return query(pos, l, mid, p << 1);
return query(pos, mid + 1, r, p << 1 | 1);
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
SegmentTreeAP seg(n);
seg.build(1, n);
while (m--) {
int opt;
cin >> opt;
if (opt == 1) {
int l, r;
ll K, D;
cin >> l >> r >> K >> D;
seg.add_ap(l, r, K, D, 1, n);
} else {
int p;
cin >> p;
cout << seg.query(p, 1, n) << '\n';
}
}
return 0;
}复杂度
- 时间:单次操作
,总 。 - 空间:线段树四倍数组(
sum/first/diff各), 。
复杂度对比
| 方案 | 单次操作 | 空间 | 常数 | 扩展性 |
|---|---|---|---|---|
| 解法一 双 Fenwick | 小 | 只能单点查询;需拆系数 | ||
| 解法二 线段树 | 略大 | 支持区间查询;不需拆系数 |
两者时间渐近相同。只做单点查询时解法一更省;需要区间求和、或懒得拆系数时,解法二更通用。
总结
"等差数列区间加"有两条经典路线:拆成"常数 + 下标 fenwick)与《线段树:区间赋值与区间查询》(模板 segtree-range-assign,本解 pull/apply/push 结构即由其改造)分别对应两种解法。
图示解析
这张 ASCII 图展示两种解法的解题路线:
朴素模拟(brute.cpp)
对 [l,r] 逐项加 K + (i-l)*D O(n) 每次操作
|
| 瓶颈:逐项访问区间,m 次操作 O(n*m) 太大
v
解法一(main.cpp,正式主解) 解法二(main-segtree.cpp)
拆系数 等差数列懒标记
K + (i-l)*D = (K - l*D) + i*D 节点存 first/diff 两个懒标记
两个区间加常数 -> 差分端点 l/r+1 整段命中: sum += len*f + d*len(len-1)/2
双 Fenwick 维护前缀和 下传时右儿子首项平移 (mid+1-l)*d
查询: a0[p] + prefix_c(p) + p*prefix_x 单点查询沿路径下传后取叶子 sum
| |
+----------+--------------------+
v
复杂度 O((n + m) log n),空间 O(n)图中上方分叉是两种优化路线的分水岭:解法一在"增量"上做代数变形,解法二在"数据结构"上扩展懒标记。共同点都是把"区间内每个位置增量不同"的困难压缩成