线段树节点同时维护区间和与平方和,用懒标记支持区间加,方差由二阶矩公式 O(log n) 求出。
OJ: luogu
题目 ID: P1471
难度:提高
标签:线段树懒标记区间加方差浮点数
日期: 2026-07-16 23:59
形式化题目
有一个长度为
- 对区间
内的每个位置加上实数 ; - 询问区间
的平均数 ; - 询问区间
的方差 。
要求按顺序处理全部操作,每次询问输出一行实数,保留 4 位小数。
思路
先看一个可以直接验证想法的朴素解:
/**
* 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:10
* update_at: 2026-08-12 22:10
*/
// brute.cpp:小数据暴力解,直接按题意逐项区间加、暴力求平均数和方差,
// 用来理解题意并辅助对拍,复杂度 O(nm),只适合小数据。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int n, m;
double 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];
}
cout << fixed << setprecision(4);
while (m--) {
int opt, l, r;
cin >> opt >> l >> r;
if (opt == 1) {
double k;
cin >> k;
// 区间加:暴力逐项加。
for (int i = l; i <= r; i++) {
a[i] += k;
}
} else {
// 查询:暴力扫描区间,同时累加和与平方和。
double s = 0, s2 = 0;
for (int i = l; i <= r; i++) {
s += a[i];
s2 += a[i] * a[i];
}
double len = r - l + 1;
double avg = s / len;
if (opt == 2) {
cout << avg << '\n';
} else {
// 方差 = E(x^2) - (E(x))^2
cout << s2 / len - avg * avg << '\n';
}
}
}
return 0;
}brute.cpp 直接维护数组:操作 1 逐项加,操作 2、3 暴力扫描区间求和与平方和,单次操作
关键观察是把方差改写成一阶矩与二阶矩的形式。设
这样查询只依赖区间的两个可合并量:和
答案是不需要。对区间整段加
另一个必须满足的是合并:push_up 时父节点要由左右子树拼出,
以样例为例,三次询问对应的矩如下:
| 步骤 | 操作 | 区间 | 和 |
平方和 |
输出 |
|---|---|---|---|---|---|
| 1 | 2 1 4 |
3.0000 |
|||
| 2 | 3 1 5 |
2.0000 |
|||
| 5 | 3 1 5 |
0.8000 |
观察第一行:平均数是
代码
/**
* 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:10
* update_at: 2026-08-15 22:00
*/
// main.cpp:P1471 正式解,线段树懒标记同时维护区间和与平方和,
// 支持区间加实数、查询区间平均值与方差,单次操作 O(log n)。
#include <bits/stdc++.h>
using namespace std;
// 区间加 + 区间和与平方和线段树(懒标记)
struct SegmentTreeRangeAdd {
using T = double;
// 线段树节点:s 为区间和,q 为平方和,lazy 为待下传的加法值
struct Node {
T s = 0; // 当前区间的真实区间和 Σa[i]
T q = 0; // 当前区间的真实平方和 Σa[i]^2
T lazy = 0; // 待下传的加法值
// 合并两个孩子:和相加、平方和相加,合并结果不携带懒标记
Node operator+(const Node &other) const {
return Node{s + other.s, q + other.q};
}
};
// 左儿子 / 右儿子的节点编号
static int lson(int p) { return p << 1; }
static int rson(int p) { return p << 1 | 1; }
// 区间 [l, r] 的中点
static int mid(int l, int r) { return (l + r) >> 1; }
int n = 0; // 区间大小
vector<Node> tree; // 线段树数组
SegmentTreeRangeAdd(int n = 0) {
init(n);
}
void init(int size) {
n = size;
tree.assign(n * 4 + 5, Node{});
}
// 上推:用两个孩子合并出当前节点
void push_up(int p) {
tree[p] = tree[lson(p)] + tree[rson(p)];
}
// 把节点 p 的整个区间 [l, r] 的每个元素都加上 val。
// 平方和:Σ(a[i]+val)^2 = Σa[i]^2 + 2*val*Σa[i] + len*val^2,
// 注意先使用旧的 s 更新 q,再更新 s。
void apply(int p, int l, int r, T val) {
int len = r - l + 1;
tree[p].q += 2 * val * tree[p].s + val * val * len;
tree[p].s += val * len;
tree[p].lazy += val;
}
// 下推:把节点 p 的加法懒标记传给两个孩子
void push_down(int p, int l, int r) {
if (tree[p].lazy == 0 || l == r) return;
int m = mid(l, r);
apply(lson(p), l, m, tree[p].lazy);
apply(rson(p), m + 1, r, tree[p].lazy);
tree[p].lazy = 0;
}
// 用数组 a 建树(下标从 1 开始)
void build(const vector<T> &a, int l, int r, int p = 1) {
if (l == r) {
tree[p].s = a[l];
tree[p].q = a[l] * a[l];
return;
}
int m = mid(l, r);
build(a, l, m, lson(p));
build(a, m + 1, r, rson(p));
push_up(p);
}
// 区间加:把 [ql, qr] 的每个元素都加上 val
void add_range(int ql, int qr, T val, int l, int r, int p = 1) {
if (ql <= l && r <= qr) {
apply(p, l, r, val);
return;
}
push_down(p, l, r);
int m = mid(l, r);
if (ql <= m) add_range(ql, qr, val, l, m, lson(p));
if (qr > m) add_range(ql, qr, val, m + 1, r, rson(p));
push_up(p);
}
// 区间查询:返回 [ql, qr] 的和与平方和
Node query(int ql, int qr, int l, int r, int p = 1) {
if (ql <= l && r <= qr) return tree[p];
push_down(p, l, r);
int m = mid(l, r);
Node answer;
if (ql <= m) answer = answer + query(ql, qr, l, m, lson(p));
if (qr > m) answer = answer + query(ql, qr, m + 1, r, rson(p));
return answer;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<double> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
SegmentTreeRangeAdd seg(n);
seg.build(a, 1, n);
cout << fixed << setprecision(4); // 输出统一保留 4 位小数
while (m--) {
int opt, l, r;
cin >> opt >> l >> r;
if (opt == 1) {
double k;
cin >> k;
seg.add_range(l, r, k, 1, n);
} else {
auto res = seg.query(l, r, 1, n);
double len = r - l + 1;
double avg = res.s / len; // 平均值 = Σa / len
if (opt == 2) {
cout << avg << '\n';
} else {
// 方差 = E(x^2) - (E(x))^2 = Σa^2/len - (Σa/len)^2
cout << res.q / len - avg * avg << '\n';
}
}
}
return 0;
}复杂度
- 时间:建树
,区间加与两种查询各 ,总 。 - 空间:
sum、sum2、lazy三个数组, 。
总结
遇到"区间修改 + 方差查询",套路是先把方差改写成一阶矩与二阶矩的组合,让查询只依赖两个满足合并律的量;再看区间加在这两个量上的更新是否封闭(apply / push / pull),本题把"赋值"语义换成"加法"并多维护一个平方和字段即可。
图示解析
这张 ASCII 图展示整道题的解题路线:
朴素模拟(brute.cpp)
区间加逐项 a[i] += k,查询暴力扫描区间求平均/方差 O(n) 每次操作
|
| 瓶颈:逐项访问区间,m 次操作 O(n*m) 太大
v
关键观察(改写方差)
s^2 = E(x^2) - (E(x))^2
查询只依赖两个可合并量:和 S = Σa_i,平方和 Q = Σa_i^2
|
| 整段加 k 是否封闭?
v
封闭性验证
S' = S + len*k
Q' = Q + 2*k*S + len*k^2 只依赖旧 (S, Q),不需要逐项信息
|
v
懒标记线段树(main.cpp)
节点维护 (sum, sum2, lazy)
完全覆盖:apply 整段更新并叠加标记
进入孩子前:push 下传标记,回溯 pull 重新合并
查询:平均数 = S/len,方差 = Q/len - (S/len)^2
|
v
复杂度 O((n + m) log n),空间 O(n)图中四层对应"暴力慢在哪"“方差如何退化成一阶、二阶矩”“整段加为何能被节点吸收”“线段树如何把修改压缩到