列表
前置知识
线段树是什么?
线段树的思想
最初的最初( pushup \text{pushup} pushup)
建树( build \text{build} build)
查询( query \text{query} query)
更新( update \text{update} update)
优化 1 1 1( lazy-tag \text{lazy-tag} lazy-tag)
优化 2 2 2(标记永久化)
最后的最后(空间提醒 & 时间复杂度说明)
代码
扩展
可持久化线段树/主席树
可持久化线段树有什么用
可持久化线段树的思想
建树( build \text{build} build)
更新( update \text{update} update)
查询( query \text{query} query)
最后(空间复杂度分析)
代码
疑问 & 催更
前置知识
- 了解位运算(为线段树优化做铺垫)。
- 了解堆。
- 了解树。
如果您还没有学完,就可以不用看下面的内容了。
线段树是什么?
-
线段树是一种可以解决区间问题的利器。
-
线段树是一种高级数据结构。
-
线段树是一种二叉搜索树(来源于网上)。
只不过以上这些都是介绍,其实真正的线段树并没有那么恐怖。
线段树的思想
线段树(segment tree),又名区间树。我们可以通过一个题目来直观的了解它:P3372。
我们按照常规思路想:
-
暴力,时间复杂度 O ( N 2 ) O(N^2) O(N2)。
-
前缀和,由于有修改操作,所以时间复杂度为 O ( N 2 ) O(N^2) O(N2)。
这个复杂度是我们不可接受的,所以我们用线段树来解决这个问题。
我们假设原来的序列为:
8
1 9 3 1 2 5 2 2
线段树,顾名思义,首先要建一棵二叉树:
一定要注意是二叉树。
这棵树按照堆式存储来编号,一共有 15 15 15 个节点。
然后我们赋予它每个节点一点值,并且把值记录为 t r e e tree tree:
每个节点旁边的一个区间代表这个节点的值是这个区间的和,例如 t r e e 5 = 4 tree_5 = 4 tree5=4。
那么区间又有什么特殊的性质呢?假设一个节点所代表的区间是 [ l , r ] [l, r] [l,r],那么它的两个子节点的区间就分别是 [ l , ( l + r > > 1 ) ] [l, (l + r >> 1)] [l,(l+r>>1)], [ ( l + r > > 1 ) , r ] [(l + r >> 1), r] [(l+r>>1),r]。就是分治的思想。
线段树的意义就讲完了,接下来将如何实现。
最初的最初( pushup \text{pushup} pushup)
pushup \text{pushup} pushup 是一个函数,表示向上更新的意思,每次可以更新节点 c u r cur cur 的值。
pushup \text{pushup} pushup 代码:
void pushup(int cur)
{
tree[cur] = tree[cur << 1] + tree[(cur << 1) + 1]; //更新
return ;
}
其实就是一个回溯操作。
建树( build \text{build} build)
这里非常简单,就是不断的分治/递归下去,最后再用 pushup \text{pushup} pushup 更新就可以了。
注意,到了叶子节点可以直接返回值。
build \text{build} build 代码:
void build(int cur, int lt, int rt) //cur 为树上节点,管辖 [lt, rt] 的数列区间
{
if(lt == rt) //递归到叶子节点
{
tree[cur] = a[lt];
return ;
}
int mid = lt + rt >> 1;
build(cur << 1, lt, mid); //继续分治/递归
build((cur << 1) + 1, mid + 1, rt);
pushup(cur); //向上传递结果
return ;
}
这里时间复杂度为 O ( 4 N ) O(4N) O(4N)。
查询( query \text{query} query)
举个例子。
就比如说查询区间 [ 3 , 8 ] [3, 8] [3,8],我们首先把问题看到节点 1 1 1。节点 1 1 1 说我管得范围太大了,得让我的两个儿子来解决。然后节点 2 2 2 说,我也解决不了,又得给我的两个儿子解决。节点 4 4 4 说这事跟我没关系,但节点 5 5 5 说我正好就是问题中的一部分,可以解决。再来看节点 3 3 3,它说我也可以正好解决。所以询问的结果就是 t r e e 5 + t r e e 3 tree_5 + tree_3 tree5+tree3。
从上述讲话中来看,其实就是不断的传儿子,只要在区间内就返回。
画图表示一下:
所以查询操作到的每个区间就只有三种选择:
- 询问的区间 [ x , y ] [x, y] [x,y] 和这个节点所覆盖的区间 [ l t , r t ] [lt, rt] [lt,rt] 根本没有关系。
图示如下:
- 询问的区间 [ x , y ] [x, y] [x,y] 和这个节点所覆盖的区间 [ l t , r t ] [lt, rt] [lt,rt] 是完全包含的关系。
图示如下:
- 问的区间 [ x , y ] [x, y] [x,y] 和这个节点所覆盖的区间 [ l t , r t ] [lt, rt] [lt,rt] 是部分包含的关系。
图示如下:
此时我们只有当区间是 3 3 3 的情形时才下传。
所以我们的代码就可以这么写:
int query(int cur, int lt, int rt, int x, int y) //cur 代表当前的节点,lt, rt 表示这个节点所管辖的区间,x, y 表示我要询问的区间
{
if(x > rt || y < lt) //都已经没有关系了,直接返回 0
{
return 0;
}
if(x <= lt && rt <= y) //是 [x, y] 的一部分
{
return tree[cur];
}
int mid = lt + rt >> 1; //向左右儿子请求答案
return query(cur << 1, lt, mid, x, y) + query((cur << 1) + 1, mid + 1, rt, x, y);
}
这里时间复杂度是 O ( log 2 N ) O(\log_2N) O(log2N) 的。