设计素材网站特点seo运营是什么意思
题目描述
有 n 个位置,编号从 1 到 n,初始所有位置的值为 0 。
需要实现以下两种操作,共调用 m 次:
- 操作 ( 1 l r v ) :将区间 [l, r] 内的每个数增加 v。
- 操作 ( 2 l r ) :返回区间 [l, r] 的累加和。
输入约束
1 ≤ n ≤ 1 0 9 1 \leq n \leq 10^9 1≤n≤109
1 ≤ m ≤ 1 0 3 1 \leq m \leq 10^3 1≤m≤103
测试链接
https://www.luogu.com.cn/problem/P2781
提示:
- 该题可以帮助学习开点线段树。
- 核心思想是仅在必要时申请空间,避免浪费存储资源。
题解:动态开点线段树
一、题目分析
该题属于 数据结构——动态开点线段树 的应用,涉及 区间修改(加法)与区间求和。
由于 ( n ) 的范围高达 ( 10^9 ),无法使用传统数组存储,需要采用动态开点线段树来解决。
二、解题思路
由于 ( n ) 过大,无法直接建立一个大小为 ( n ) 的数组,因此使用 动态开点线段树,即仅在需要时才创建节点,减少空间浪费。
-
线段树核心操作
add(l, r, v)
:将区间 ([l, r]) 内的所有数增加 ( v )。querySum(l, r)
:查询区间 ([l, r]) 的累加和。
-
关键技巧
- 动态开点:只在访问到某个区间时才创建子节点,避免对所有 ( n ) 个位置预分配存储。
- 延迟标记(懒惰更新):
- 对某个区间进行加法操作时,不直接递归更新所有子节点,而是打上标记,等到查询或细分区间时再下传标记,减少操作次数,提高效率。
三、代码实现
#include <bits/stdc++.h>#define ll long long
using namespace std;// 预估节点数:操作次数 * 树高 * 2(控制空间开销)
const int N = 8E4 + 10; int cur = 1; // 当前使用的节点索引
struct Node {int left, right; // 左右子节点索引int add; // 延迟标记(懒惰标记)ll sum; // 区间和
} tree[N];// **延迟更新(lazy propagation)**
void lazyAdd(int p, int l, int r, int v) {tree[p].sum += 1LL * v * (r - l + 1); // 更新区间总和(避免溢出)tree[p].add += v; // 记录懒惰标记
}// **上推更新**(合并子区间信息)
void pushup(int p) {int lp = tree[p].left, rp = tree[p].right;tree[p].sum = tree[lp].sum + tree[rp].sum;
}// **下推懒惰标记**
void pushdown(int p, int l, int r) {if (tree[p].add) { // 只有当 add 不为 0 时才需要下推int mid = (l + r) >> 1;if (tree[p].left == 0) tree[p].left = ++cur;lazyAdd(tree[p].left, l, mid, tree[p].add);if (tree[p].right == 0) tree[p].right = ++cur;lazyAdd(tree[p].right, mid + 1, r, tree[p].add);tree[p].add = 0; // 清除当前节点的懒惰标记}
}// **区间加法(更新)**
void add(int p, int l, int r, int jobl, int jobr, int v) {if (jobl <= l && r <= jobr) { // 完全覆盖,直接更新lazyAdd(p, l, r, v);} else { pushdown(p, l, r); // 先下推懒惰标记int mid = (l + r) >> 1;// 左子区间if (jobl <= mid) {if (tree[p].left == 0) tree[p].left = ++cur;add(tree[p].left, l, mid, jobl, jobr, v);}// 右子区间if (mid < jobr) {if (tree[p].right == 0) tree[p].right = ++cur;add(tree[p].right, mid + 1, r, jobl, jobr, v);}pushup(p); // 更新父节点值}
}// **区间查询(求和)**
ll querySum(int p, int l, int r, int jobl, int jobr) {if (jobl <= l && r <= jobr) { // 完全覆盖,直接返回return tree[p].sum;} else {pushdown(p, l, r); // 先下推懒惰标记int mid = (l + r) >> 1;ll sum = 0;if (jobl <= mid && tree[p].left != 0) { sum += querySum(tree[p].left, l, mid, jobl, jobr);}if (mid < jobr && tree[p].right != 0) { sum += querySum(tree[p].right, mid + 1, r, jobl, jobr);}return sum;}
}int main() {int n, m;cin >> n >> m;for (int i = 0, op, l, r, k; i < m; i++) {cin >> op >> l >> r;if (op == 1) { // 区间增加cin >> k;add(1, 1, n, l, r, k);} else { // 区间求和cout << querySum(1, 1, n, l, r) << endl;}}return 0;
}
四、时间 & 空间复杂度分析
时间复杂度
-
add(l, r, v)
(区间更新):- 最坏情况下,树高为 ( O(log n) )。
- 由于动态开点,仅访问实际需要的节点,因此时间复杂度接近 ( O(log n) )。
-
querySum(l, r)
(区间查询):- 同样最多访问 ( O(log n) ) 个节点。
- 时间复杂度 ( O(log n) )。
-
整体复杂度:
- 单次操作:( O(log n) )
- 总共 ( m ) 次操作:( O(m log n) )
空间复杂度
- 数组方式:如果直接用数组存储,需要 ( O(n) ) 的空间,但 ( n ) 过大无法接受。
- 动态开点方式:
- 仅在需要时申请空间。
- 最坏情况:如果操作覆盖整个 ( n ),最多会创建 ( O(m log n) ) 个节点。
- 实际情况:一般远小于 ( n )。
- 空间复杂度:( O(m log n) )
五、总结
-
为什么需要动态开点?
- ( n ) 太大,无法直接用数组存储数据。
- 通过按需分配节点,减少空间浪费。
-
如何优化区间更新?
- 懒惰标记(Lazy Propagation) 避免不必要的递归更新,提高效率。
-
适用场景
- 适用于 稀疏修改,即数据范围大但实际操作区域较少的情况。
- 适用于 区间修改 & 区间查询 结合的问题。
本题重点在于 动态开点线段树 + 懒惰标记优化,是处理超大范围数据的关键技巧!