为什么要学习非递归线段树,这个问题大部分博客解释为普通线段树常数大,会被卡常,即虽然都是O(logn),其实在一些具有可加性的问题中树状数组一般跑的比线段树快。于我个人而言,原因有二:一是我冥冥之中有一种感觉,线段树和树状数组本质上没有什么区别(这个就比较玄学);二是受FFT中位逆序置换启发(但是还没有学会FFT),线段树也是一种分治,甚至是很完美的那种,从递归到非递归毫无疑问是一种优化方案。所以当我看到zkw学长的线段树讲义,他里面提到线段树可以是树状数组,平衡树等我颇感兴趣(三十六方,必为大统)
  话不多说,我们直接开始学习(建议先把握普通线段树再学,可能大部分题目不是必须要非递归,但是可以加深理解,而且一些处理挺有意思的)
  按照常用模板来讲解

  一:单点修改,区间查询

  (这是我认为最好的图解,请按我的解释仔细体味,后面本人没准备图,全靠在这上面脑测)
  如图,这是一个在维护一个长度为8的数组,采用的是堆式存储的方法,故维护线段树的数组要开4N(N为原数组长度),与普通的线段树不同的是他不是从第一片叶子开始存储的,而是在第二片,与之对应的是图上2个红方框的元素,我们称其为哨兵,为什么要这2个哨兵呢?这是为了实现区间查询,后文解释

  建树:

  上图我们知道叶子数为N + 2(含哨兵),但是我们是要将它补为完全二叉树(有很多优秀性质)来实现迭代更新操作,所以实际叶子数应该是大于等于N + 2的最小的2的整数次幂,记为bit,则根据二叉树的性质,非叶子节点数有bit - 1个,由于第一片叶子是哨兵(数组下标为bit),所以我们直接从bit + 1开始存(是不是很好的性质啊),先求下bit:
  for(bit = 1; bit < n + 2; bit <<= 1);  代码中n为数组大小,然后bit和后续操作息息相关,我们设为全局变量
  存储叶子节点:  for(int i = 1; i <= n; i++) dat[bit + i] = a[i];  a数组即为原数组
  然后直接向上传递即可:
  for(int i = bit - 1; i; i--)  dat[i] = dat[i << 1] + dat[i << 1 | 1];

void build(int n){
  for(bit = 1; bit < n + 2; bit <<= 1);
  for(int i = 1; i <= n; i++) dat[bit + i] = read();//这是个快读,因为一些题中原数组用处不大,看个人习惯吧,因人和题目而异
  for(int i = bit - 1; i; i--) dat[i] = dat[lc(i)] + dat[rc(i)];
  }
  那么就建完了,把握住逻辑甚至比递归还好写

  单点修改:

  单点修改也是很简单,因为我们根据堆式存储和bit已经可以刻画出线段树了,所以更新就是直接定位修改,然后向上传递
  
void modify(int x, int v){
  for(dat[x += bit] += v, x >>= 1; x; x >>= 1) dat[x] = dat[lc(x)] + dat[rc(x)];//x 是元素在原数组的位置,结合前面的分析bit + x就是在线段树中的位置,后面不在提及。然后不断跳到父节点修改
  }
 
  这里实现的是单点增加和求区间和的操作,具体因题目而异

  区间查询:

  哨兵启动!线段树一个优势就是它把区间划分便于操作的同时能应对所有的问询,而在递归版中我们可以很自然的理解方法,放到迭代如何呢?zkw学长采用了一种很聪明的方法,利用包裹的方法,具体的我们结合代码分析(如果学过LCA会好理解一点个人感觉)
  

int quiry(int l, int r){//不要吐槽我的命名啦
    int ans = 0;
    for(l += (bit - 1), r += (bit + 1); l ^ r ^ 1; l >>= 1, r >>= 1){//传入的参数中[l,r]是待查询区间,而我们定位到的(l, r)是一个包含代查询区间的最小开区间(左右即为哨兵),原因?先接着看下去;l ^ r ^ 1,这是判断是否已经完全覆盖到了要查询的区间,即左右哨兵成为了兄弟(兄弟节点异或结果为1,再异或1为0退出)
      if(~l & 1) ans += dat[l ^ 1];//如果左边的哨兵是左子树,因为最小包含的缘故,其右子树必然在区间内,累加入答案,不熟悉位运算的可能要理解下
      if(r & 1) ans += dat[r ^ 1];//右哨兵同理
    }
    return ans;
  }
  结合图片实例体会:
  假设我们要查询的就是[2,7],那么定位后的l = 17, r = 24,二者不符合累加条件,跳到父节点
  我们发现r无论如何不能满足
  l = 8,是左子树,故其右子树9包含的元素累加, r=12
  l = 4,继续加节点5包含的元素, r = 6
  l = 2, r = 3,变成兄弟了,退出
完整代码:  

#include<iostream>
using namespace std;
int read(){
int f = 1, x = 0;
char ch = getchar();
while(ch < '0' || ch > '9'){
if(ch == '-') f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9'){
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return f * x;
}
const int N = 5e5 + 3;
int dat[N * 2], bit;
#define lc(x) x << 1
#define rc(x) (x << 1) | 1
void build(int n){
for(bit = 1; bit < n + 2; bit <<= 1);
for(int i = 1; i <= n; i++) dat[bit + i] = read();
for(int i = bit - 1; i; i--) dat[i] = dat[lc(i)] + dat[rc(i)];
}
void modify(int x, int v){
for(dat[x += bit] += v, x >>= 1; x; x >>= 1) dat[x] = dat[lc(x)] + dat[rc(x)];
}
int quiry(int l, int r){
l = bit + l - 1, r = bit + r + 1;
int ans = 0;
while(l ^ r ^ 1){
if(~l & 1) ans += dat[l ^ 1];
if(r & 1) ans += dat[r ^ 1];
l >>= 1;
r >>= 1;
}
return ans;
}
signed main(){
int n = read(), q = read();
build(n);
while(q--){
int op = read(), x = read(), y = read();
if(op == 1) modify(x, y);
else printf("%d\n",quiry(x, y));
}
return 0;
}

区间修改,区间查询(单点查询直接令查询区间是[x,x]就行)  

struct seg{
  int dat, lzy;
  #define dat(x) tr[x].dat
  #define lzy(x) tr[x].lzy
 }tr[N << 2];
 #define lc(x) x << 1
 #define rc(x) (x << 1) | 1

  先展示下定义  

建树变化不大:

void build(int n){
for(bit = 1; bit < n + 2; bit <<= 1);
for(int i = 1; i <= n; i++) dat(bit + i) = read();
for(int i = bit - 1; i; i--) dat(i) = dat(lc(i)) + dat(rc(i));
}

区间修改:

修改自下而上记录下左右哨兵包含的区间内有多少个元素被覆盖即可

void modify(int l, int r, int v){
int llen = 0, rlen = 0, len = 1;//左右哨兵的被覆盖长度以及向上跳跃后节点区间变化
for(l += (bit - 1), r += (bit + 1); l ^ r ^ 1; l >>= 1, r >>= 1, len <<= 1){
if(~l & 1) lzy(l ^ 1) += v, llen += len;//标记永久化
if(r & 1) lzy(r ^ 1) += v, rlen += len;
dat(l >> 1) += v * llen, dat(r >> 1) += v * rlen; //如果l一直是右子树这llen为0,否则它的父节点的值应该加上左子树的区间增量,r同理
}
for(llen += rlen, l >>= 1; l; l >>= 1) dat(l) += v * llen;//当兄弟相遇直接一路传递到根就行
}

区间查询:

和上面差不多,加上lzy的增量即可

int quiry(int l, int r){
int llen = 0, rlen = 0, len = 1, ans = 0;
for(l += (bit - 1), r += (bit + 1); l ^ r ^ 1; l >>= 1, r >>= 1, len <<= 1){
if(~l & 1) ans += dat(l ^ 1) + lzy(l ^ 1) * len, llen += len;
if(r & 1) ans += dat(r ^ 1) + lzy(r ^ 1) * len, rlen += len;
ans += lzy(l >> 1) * llen + lzy(r >> 1) * rlen;
//if判断为真值的必然是全部包含在待查询区间的,因此直接乘len即可,而当llen和rlen不为0且哨兵的父节点带有懒标记时,答案应加上哨兵的兄弟被包含在区间里的长度
}
for(llen += rlen, l >>= 1; l; l >>= 1) ans += lzy(l) * llen;//因为标记是modify从哨兵一路上传,l,r之间的区间不会有标记,但是含lzy的区间一定是全部有增量,所以要一路上溯累加
return ans;
}

完整代码在这里:

#include<iostream>
using namespace std;
#define int long long
int read(){
int f = 1, x = 0;
char ch = getchar();
while(ch < '0' || ch > '9'){
if(ch == '-') f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9'){
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return f * x;
}
const int N = 1e6 + 3;
struct seg{
int dat, lzy;
#define dat(x) tr[x].dat
#define lzy(x) tr[x].lzy
}tr[N << 2];
int bit;
#define lc(x) x << 1
#define rc(x) (x << 1) | 1
void build(int n){
for(bit = 1; bit < n + 2; bit <<= 1);
for(int i = 1; i <= n; i++) dat(bit + i) = read();
for(int i = bit - 1; i; i--) dat(i) = dat(lc(i)) + dat(rc(i));
}
void modify(int l, int r, int v){
int llen = 0, rlen = 0, len = 1;
for(l += (bit - 1), r += (bit + 1); l ^ r ^ 1; l >>= 1, r >>= 1, len <<= 1){
if(~l & 1) lzy(l ^ 1) += v, llen += len;
if(r & 1) lzy(r ^ 1) += v, rlen += len;
dat(l >> 1) += v * llen, dat(r >> 1) += v * rlen;
}
for(llen += rlen,l >>= 1; l; l >>= 1) dat(l) += v * llen;
}
int quiry(int l, int r){
int llen = 0, rlen = 0, len = 1, ans = 0;
for(l += (bit - 1), r += (bit + 1); l ^ r ^ 1; l >>= 1, r >>= 1, len <<= 1){
if(~l & 1) ans += dat(l ^ 1) + lzy(l ^ 1) * len, llen += len;
if(r & 1) ans += dat(r ^ 1) + lzy(r ^ 1) * len, rlen += len;
ans += lzy(l >> 1) * llen + lzy(r >> 1) * rlen;
}
for(llen += rlen, l >>= 1; l; l >>= 1) ans += lzy(l) * llen;
return ans;
}
signed main(){
int n = read(), q = read();
build(n);
while(q--){
int op = read();
if(op == 1){
int l = read(), r = read(), k = read();
modify(l, r, k);
}
else{
int x = read();
printf("%lld\n",quiry(x, x));
}
}
return 0;
}

根据某校OJ来看非递归区间加大概比递归快了3倍
总体而言,现在线段树在时间上优化有zkw线段树,空间上有动态开点
参考链接:(如有介意,联系我删除)
主要模板块学习

插图

原讲义

zkw线段树--非递归线段树的更多相关文章

  1. 菜鸟笔记:node.js+mysql中将JSON数据构建为树(递归制作树状菜单数据接口)

    初学Web端开发,今天是第一次将所学做随笔记录,肯定存在多处欠妥,望大家海涵:若有不足,望大家批评指正. 进实验室后分配到的第一个项目,需要制作一个不确定层级树形菜单的数据接口,对于从来没实战编过程的 ...

  2. ZKW线段树 非递归版本的线段树

    学习和参考 下面是支持区间修改和区间查询的zkw线段树模板,先记下来. #include <algorithm> #include <iterator> #include &l ...

  3. [模板]非递归线段树(zkw的变异版本)

    类似于zkw,但空间只用两倍,zkw要4倍. 链接 可以下传标记,打熟后很好码. #include <set> #include <cmath> #include <cs ...

  4. hdu 3887 Counting Offspring(DFS序【非递归】+树状数组)

    题意: N个点形成一棵树.给出根结点P还有树结构的信息. 输出每个点的F[i].F[i]:以i为根的所有子结点中编号比i小的数的个数. 0<n<=10^5 思路: 方法一:直接DFS,进入 ...

  5. vue.js 树菜单 递归组件树来实现

    树形视图 Example: https://vuefe.cn/v2/examples/tree-view.html 参照前辈方法实现的,觉得不错,记录一下: 父组件: <!-- 菜单树 --&g ...

  6. BZOJ 3744 Gty的妹子序列 (分块+树状数组+主席树)

    题面传送门 题目大意:给你一个序列,多次询问,每次取出一段连续的子序列$[l,r]$,询问这段子序列的逆序对个数,强制在线 很熟悉的分块套路啊,和很多可持久化01Trie的题目类似,用分块预处理出贡献 ...

  7. 线段树(单标记+离散化+扫描线+双标记)+zkw线段树+权值线段树+主席树及一些例题

    “队列进出图上的方向 线段树区间修改求出总量 可持久留下的迹象 我们 俯身欣赏” ----<膜你抄>     线段树很早就会写了,但一直没有总结,所以偶尔重写又会懵逼,所以还是要总结一下. ...

  8. [bzoj3196][Tyvj1730]二逼平衡树_树套树_位置线段树套非旋转Treap/树状数组套主席树/权值线段树套位置线段树

    二逼平衡树 bzoj-3196 Tyvj-1730 题目大意:请写出一个维护序列的数据结构支持:查询给定权值排名:查询区间k小值:单点修改:查询区间内定值前驱:查询区间内定值后继. 注释:$1\le ...

  9. hdu5044 Tree 树链拆分,点细分,刚,非递归版本

    hdu5044 Tree 树链拆分.点细分.刚,非递归版本 //#pragma warning (disable: 4786) //#pragma comment (linker, "/ST ...

  10. 线段树&&线段树的创建线段树的查询&&单节点更新&&区间更新

    目录 线段树 什么是线段树? 线段树的创建 线段树的查询 单节点更新 区间更新 未完待续 线段树 实现问题:常用于求数组区间最小值 时间复杂度:(1).建树复杂度:nlogn.(2).线段树算法复杂度 ...

随机推荐

  1. Lynx-字节跳动跨平台框架多端兼容Android, iOS, Web 原生渲染

    介绍 字节跳动近期开源的跨平台框架Lynx被视为一项重要的技术创新.相较于市场上已有的解决方案如React Native (RN) 和Flutter,Lynx具有独特的特性. 首先,Lynx采用轻量级 ...

  2. 自实现模态对话框-DoModal函数

    参考CDialog::DoModal函数的实现方式,自己实现了模态框相关功能. ModalBase.h头文件 1 #include <afxwin.h> 2 3 #define ID_NU ...

  3. 重磅消息,微软宣布 VS Code Copilot 开源,剑指 Cursor!

    前言 微软宣布重磅消息将把 GitHub Copilot Chat 扩展的代码以 MIT 许可证协议开源,然后将扩展中的 AI 功能重构到 VS Code 核心中,这一举措是为了将 VS Code 成 ...

  4. 异步日志分析:MongoDB与FastAPI的高效存储揭秘

    title: 异步日志分析:MongoDB与FastAPI的高效存储揭秘 date: 2025/05/22 17:04:56 updated: 2025/05/22 17:04:56 author: ...

  5. 新纪元:"老"新人

    博客园注册很久了,但从未发布过内容.终于开通博客,记录自己,也支持博客园! 另外,这次苹果秋季发布会真的好无聊!︎

  6. React Native开发鸿蒙Next---RN键盘问题

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  7. Qt图像处理技术一:对QImage图片美颜,使用双指数滤波

    一.效果图 二.demo源码地址(除了磨皮还有一些基本的滤镜) 如果你觉得有用的话,期待你的小星星 实战应用项目: github :https://github.com/dependon/simple ...

  8. Java线程状态和状态切换

    背景   先来探讨一个关于多线程的基础知识:java线程有多少种状态?根据JDK定义,答案是六种!为什么很多人给出的答案却是五种呢?这极有可能是将操作系统层面的线程状态和Java线程状态混为一谈了.因 ...

  9. Java Solon v3.3.2 发布(可替换,美国博通公司的 Spring 方案)

    Solon 框架! Solon 是新一代,Java 企业级应用开发框架.从零开始构建(No Java-EE),有灵活的接口规范与开放生态.采用商用友好的 Apache 2.0 开源协议,是" ...

  10. 记录.Net 8 发布增加 PublishTrimmed 裁剪选项,调用WMI 的ManagementObject 异常

    最近在做OTA的功能,需要获取到sn做一些业务的逻辑.我们自己实现的库里边的,大部分都是调用 System.Management 的 ManagementObjectSearcher 获取 Bios ...