同步发表于 优快云@return_dr
这一篇文章我们将对线段树中的常规操作进行详细的讨论。
- 以下所提到的复杂度如无特殊说明均为时间复杂度。 log \log log 的底数均为 2 2 2。
不开 long long 见祖宗!
文章目录
第一部 普通线段树
一、引入
对于一串数,举例为 a a a,需要实现以下几种操作(修改操作均为加减法):
- 单点修改
- 单点查询
- 区间修改
- 区间查询(区间和)
显然 1 1 1 与 2 2 2 被 3 3 3 与 4 4 4 包含。但为了讨论的清晰,我们也将对其单独讨论。
如果我们用常规方式(数组)存储这串数,其查询操作为 O ( 1 ) O(1) O(1),更改操作为 O ( 1 ) O(1) O(1),求和操作为 O ( l ) O(l) O(l)( l l l 为所求序列长度)。
#define LL long long // 不开 long long 见祖宗!
#define ref(i, a, b, p) for (signed(i) = (a); (i) <= signed(b); (i) += signed(p))
const int maxn = 50005;
LL a[maxn], n, q, sum;
void work()
{
cin >> n;
ref (i, 1, n, 1)
cin >> a[i];
cin >> q;
ref (__, 1, q, 1)
{
int x, k, l, r;
cin >> x;
if (x == 1) // 以下均如题
{
cin >> k;
a[x] = k;
}
else if (x == 2)
cout << a[x] << endl;
else if (x == 3)
{
cin >> l >> r >> k;
ref (i, l, r, 1)
a[x] += k;
}
else
{
sum = 0;
cin >> l >> r;
ref (i, l, r, 1)
sum += a[i];
cout << sum << endl;
}
}
}
如果用前缀和来操作的话,虽然求和操作为 O ( 1 ) O(1) O(1),但是更改数值后更新前缀和的复杂度为 O ( n ) O(n) O(n),没有起到太大的优化效果,不再放代码。
有人说 O ( n ) O(n) O(n) 的时间复杂度已经很优了,但是设想,如果有 q q q 次询问,那么总的复杂度为 O ( n q ) O(nq) O(nq),不可接受。
二、优化方案
经过前面优先队列等数据结构的铺垫,我们知道,树是一种能在复杂度为 O ( log n ) O(\log n) O(logn) 的情况下处理大部分操作的数据结构。
那么我们可以这样想:对于一串数 a a a,我们把它看成一条长度为 n n n 的线段,标记线段左端点为 1 1 1,右端点为 n n n,那么我们让线段上的 1 1 1 对应 a 1 a_1 a1,让 2 2 2 对应 a 2 a_2 a2……让 n n n 对应 a n . a_n. an.,这样我们就用一条线段表示出了 a a a。
我们此时建立一棵完全二叉树(偶数个数的情况下为满二叉树),以刚刚建立的线段上的每一个点(从 a 1 a_1 a1 到 a n a_n an)为叶子节点,用父节点表示其左右儿子的和,那么根节点就为整条线段(也就是整串数)的和。
具体来看(代码放到最后):
建树时,先建立叶子节点,然后逐层回溯计算父结点的值,每个节点需要存储的变量有它的值以及左右端点(就是区间范围)。时间复杂度 O ( n log n ) O(n\log n) O(nlogn)。
如下图,先建立节点 1 , 2 , 3 … 8 1,2,3\dots8 1,2,3…8(代表 a 1 , a 2 , a 3 … a 8 a_1,a_2,a_3\dots a_8 a1,a2,a3…a8,下同),然后逐层回溯更新节点 a , b , c … g a,b,c\dots g a,b,c…g,更改顺序:红、黄、绿、蓝。
在此图中:
a = 1 + 2 , b = 3 + 4 , c = 5 + 6 , d = 7 + 8. a=1+2,\,b=3+4,\,c=5+6,\,d=7+8. a=1+2,b=3+4,c=5+6,d=7+8.
e = a + b = ∑ i = 1 4 a i , f = c + d = ∑ i = 5 8 a i . e=a+b=\sum\limits_{i=1}^{4}a_i,\,f=c+d=\sum\limits_{i=5}^{8}a_i. e=a+b=i=1∑4ai,f=c+d=i=5∑8