人傻常数大,胸牌抱回家。
再谈线段树
我们所常知的线段树,是一种基于分治和递归的数据结构,通过合并子区间来得到答案,具有合并性,操作时间复杂度为
但由于众所周知的原因,递归的常数会比递推大很多,这也是线段树常数远远大于树状数组的原因(两者时间复杂度皆为
所以,或许我们可以深挖线段树的性质来优化下常数。
非递归式线段树
zkw线段树
由清华大学的张昆玮研究出的不基于递归与分治的线段树,常数极小,码量也较小。但扩展性(可做性)没有递归式线段树优秀。一般称为
理论上来讲,
运行形式
我们深究线段树的性质,发现一些比较巧妙的事,如当
容易发现,每一个叶结点对应的线段树结点满足
但不仅仅与此,一棵满二叉树构造的线段树还有以下性质:
- 整棵树有
个结点; - 一共有
层,第 层有 个结点,表示线段长度为 ,其中 。 - 如果
为兄弟结点(位于同一层),则有 。 - 左儿子结点编号为奇数,右儿子结点编号为偶数。
但很可惜的是,这些性质都局限于了
操作实现
注意一点是,
建树
可以省去 struct
和原数组,直接对线段树数组进行赋值。
参考实现
1 | inline void build() |
单点修改
直接进行暴力 pushup
,可以在
参考实现
1 | inline void modifyX(int x,int v) |
单点修改区间查询
这里以查询区间和为例,回收伏笔,我们要求将
所以我们在线段树的叶结点上建立两个指针
但显然地,当
参考实现
1 | inline int querySum(int l,int r) |
区间修改
容易发现,逆向操作的 pushdown
的时候很难实现,毕竟是由下至上的传递。所以需要用到万恶的标记永久化,这个操作我们在分块的时候也提及到过。
对于每一个结点记录 add[]
表示当前结点曾经被修改过的和。但容易发现,按照上述区间修改的操作来讲,我们并不保证该区间是被完整访问的,这也是线段树需要拆分区间的原因,所以我们需要记录一个 std::pair
来保存该结点所表示的区间,但那样就体现不出
参考实现
1 | inline void modifyAdd(int l,int r,ll v) |
注意到,这一次的向上传递必须要到达根结点,因为区间修改下并不保证会在该子区间停止。
区间修改区间查询
同理区间修改,把 add[]
整上 ln,rn
后加入答案。
参考实现
1 | inline ll querySum(int l,int r) |
扩展
zkw 求最大子段和
题目简介
题目名称:小白逛公园
题目来源:
评测链接:https://www.luogu.com.cn/problem/P4513
形式化题意:给定一个长度为
,将 ,求
数据范围:
这是一个极其经典的线段树的题,当然也可以用
对于普通线段树而言,我们需要维护
合并时,令
则有:
可以在
这一次让我们尝试用
容易发现,区间最大子段和的合并是有优先级的,这个优先级并没有体现在运算中,而是左和右的优先级上,我们需要的是左端点的
所以我们需要运用一些手段使其满足这一点。常用的方法是,将所有需要计算的区间存储下来,然后从左往右(或者从右往左)依次进行合并。左端点用 std::queue
,右端点用 std::stack
,就可以保证这一点。
AC Code
1 | // ----- Eternally question----- |