上帝造题的七分钟(线段树 + 区间开方 + 势能分析剪枝)
上帝造题的七分钟(线段树 + 区间开方 + 势能分析剪枝)
一、题目描述
维护一个正整数数列,支持两种操作:
0 l r:把区间[l, r]中的每个数开平方(下取整)1 l r:询问区间[l, r]的和
注意:输入中可能出现 l > r,此时要交换 l 和 r。
二、思路分析
直接做的问题
区间开方不像区间加、区间乘那样可以直接用懒标记处理,因为:
sqrt(a + b) ≠ sqrt(a) + sqrt(b)
无法在O(1)的时间快速求解
所以无法把「开方」这个操作直接打在懒标记上。
关键观察
一个数 x 反复开平方,会迅速变小:
10^12开方 1 次 →10^6- 开方 2 次 →
10^3 - 开方 3 次 →
31 - 开方 4 次 →
5 - 开方 5 次 →
2 - 开方 6 次 →
1
到 1 之后,再怎么开方还是 1,不再变化。
所以每个数最多被有效开方 6 次左右,之后就是无效操作。
优化策略
线段树每个节点额外维护一个 mx(区间最大值):
- 如果
mx <= 1,说明这个区间所有数都已经是1,开方没有意义,直接返回 - 否则继续递归
这样每个位置最多被真正更新 6 次,总复杂度 O(n log n × 6)。
三、算法流程
- 建树,每个节点维护
sum(区间和)和mx(区间最大值) - 对每个操作:
- op = 0(开方):
- 如果
seg[p].mx <= 1,直接返回 - 如果是叶子节点,
sum = mx = sqrt(mx) - 否则递归左右儿子,再
push_up
- 如果
- op = 1(查询):
- 标准区间查询和
- op = 0(开方):
四、AC 代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
ll a[N];
int n;
struct node
{
int L, R;
ll mx; // 区间最大值
ll sum; // 区间和
} seg[N * 4];
// 向上合并
void push_up(int p)
{
seg[p].mx = max(seg[p << 1].mx, seg[p << 1 | 1].mx);
seg[p].sum = seg[p << 1].sum + seg[p << 1 | 1].sum;
}
// 建树
void build(int p, int L, int R)
{
seg[p].L = L, seg[p].R = R;
if (L == R)
{
seg[p].mx = seg[p].sum = a[L];
return;
}
int mid = (L + R) >> 1;
build(p << 1, L, mid);
build(p << 1 | 1, mid + 1, R);
push_up(p);
}
// 区间开方
void update(int p, int L, int R)
{
// 关键剪枝:区间最大值 <= 1,开方无意义
if (seg[p].mx <= 1) return;
int l = seg[p].L, r = seg[p].R;
// 叶子节点:直接开方
if (l == r)
{
seg[p].sum = seg[p].mx = sqrt(seg[p].mx);
return;
}
int mid = (l + r) >> 1;
if (L <= mid) update(p << 1, L, R);
if (R > mid) update(p << 1 | 1, L, R);
push_up(p);
}
// 区间求和
ll query(int p, int L, int R)
{
int l = seg[p].L, r = seg[p].R;
if (L <= l && r <= R) return seg[p].sum;
int mid = (l + r) >> 1;
ll res = 0;
if (L <= mid) res += query(p << 1, L, R);
if (R > mid) res += query(p << 1 | 1, L, R);
return res;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
build(1, 1, n);
int m;
cin >> m;
int op, l, r;
while (m--)
{
cin >> op >> l >> r;
if (l > r) swap(l, r); // 题目可能出现 l > r
if (op == 1)
cout << query(1, l, r) << endl;
else
update(1, l, r);
}
return 0;
}
五、复杂度
| 操作 | 时间复杂度 |
|---|---|
| 建树 | O(n) |
| 单次开方 | 均摊 O(log n) |
| 单次查询 | O(log n) |
| 总复杂度 | O((n + m) log n) |
每个数最多被有效开方 6 次(因为 10^12 开 6 次方就到 1),所以开方操作的总代价是 O(6n log n),完全可以接受。
六、相关题目
- 洛谷 P4145 花神游历各国 — 同一道题
- 洛谷 P2572 [SCOI2010] 序列操作 — 线段树区间操作
七、总结
| 要点 | 内容 |
|---|---|
| 核心难点 | 区间开方不能用懒标记直接处理 |
| 关键观察 | 一个数最多有效开方 6 次,到 1 后不再变化 |
| 优化方式 | 维护区间最大值,mx <= 1 时剪枝 |
| 复杂度 | 均摊 O((n + m) log n) |

浙公网安备 33010602011771号