关于一种比较特别的线段树写法

这篇NOIP水平的blog主要是为了防止我AFO后写法失传而写的(大雾)

前言

博主平常写线段树的时候经常用一种结构体飞指针的写法, 这种写法具有若干优势:

  • 条理清晰不易写挂, 且不需要借助宏定义就可以实现这一点
  • 可以在很小的修改的基础上实现线段树的各种灵活运用, 比如:
    • 可持久化
    • 动态开点
    • 线段树合并
  • 出错会报RE方便用gdb一类工具快速定位错误(平衡树也可以用类似写法, 一秒定位板子错误)
  • 而且将线段树函数中相对比较丑陋的部分参数隐式传入, 所以(可能)看上去比较漂亮一些
  • 在使用内存池而不是动态内存的情况下一般比普通数组写法效率要高
  • 原生一体化, 在数据结构之间嵌套时可以直接套用而不必进行各种兼容性修改
  • 接口作为成员函数出现, 不会出现标识符冲突(重名)的情况

下面就以线段树最基础的实现例子: 在 \(O(n+q\log n)\) 的时间复杂度内对长度为 \(n\) 的序列进行 \(q\) 次区间加法区间求和为例来介绍一下这种写法.

对某道题目的完整实现或者其他的例子可以参考我的其他博文中的附带代码或者直接查询我在UOJ/LOJ的提交记录.

(可能我当前的写法并没有做到用指针+结构体所能做到的最优美的程度而且没有做严格封装, 求dalao轻喷)

注意这篇文章的重点是写法而不是线段树这个知识点qwq...

前置技能是要知道对某个对象调用成员函数的时候有个 this 指针指向调用这个函数的来源对象.

定义

定义一个结构体 Node 作为线段树的结点. 这个结构体的成员变量与函数定义如下:

struct Node{int l;int r;int add;int sum;Node* lch;Node* rch;Node(int,int);void Add(int);void Maintain();void PushDown();int Query(int,int);void Add(int,int,int);
};

其中:

  • lr 分别表示当前结点所代表的区间的左右端点
  • add 是区间加法的惰性求值标记
  • sum 是当前区间的和
  • lchrch 分别是指向当前结点的左右子结点的指针
  • Node(int,int) 是构造函数, 用于建树
  • void Add(int d) 是一个辅助函数, 将当前结点所代表的区间中的值都加上 \(d\).
  • void Maintain() 是用子结点信息更新当前结点信息的函数
  • void PushDown() 是下传惰性求值标记的函数
  • int Query(int l,int r) 对区间 \([l,r]\) 求和
  • void Add(int l,int r,int d) 对区间 \([l,r]\) 中的值都加上 \(d\).

建树

个人一般选择在构造函数中建树. 写法如下(此处初值为 \(0\)):

Node(int l,int r):l(l),r(r),add(0),sum(0){if(l!=r){int mid=(l+r)>>1;this->lch=new Node(l,mid);this->rch=new Node(mid+1,r);this->Maintain(); // 因为初值为 0 所以此处可以不加}
}

这个实现方法利用了 new Node() 会新建一个结点并返回一个指针的性质递归建立了一棵线段树.

new Node(l,r) 实际上就是建立一个包含区间 \([l,r]\) 的线段树. 其中 \(l\) 和 \(r\) 在保证 \(l\le r\) 的情况下可以任意.

注意到我在 \(l=r\) 的时候并没有对 lchrch 赋值, 也就是说是野指针. 为什么保留这个野指针不会出现问题呢? 我们到查询的时候再做解释.

实际使用的时候可以这样做:

int main(){Node* Tree=new Node(1,n);
}

然后就可以建立一棵包含区间 \([1,n]\) 的线段树了.

区间加法

在这个例子中要进行的修改是 \(O(\log n)\) 时间复杂度内的区间加法, 那么需要先实现惰性求值, 当操作深入到子树中的时候下传标记进行计算.

惰性求值

首先实现一个小的辅助函数 void Add(int):

void Add(int d){this->add+=d;this->sum+=(this->r-this->l+1)*d;
}

作用是给当前结点所代表的区间加上 \(d\). 含义很明显就不解释了.

有了这个小辅助函数之后可以这样无脑地写 void PushDown():

void PushDown(){if(this->add!=0){this->lch->Add(this->add);this->rch->Add(this->add);this->add=0;}
}

这两个函数中所有 this-> 因为没有标识符重复的情况其实是可以去掉的, 博主的个人习惯是保留.

维护

子树修改后显然祖先结点的信息是需要更新的, 于是这样写:

void Maintain(){this->sum=this->lch->sum+this->rch->sum;
}

修改

主要的操作函数可以写成这样:

void Add(int l,int r,int d){if(l<=this->l&&this->r<=r)this->Add(d);else{this->PushDown();if(l<=this->lch->r)this->lch->Add(l,r,d);if(this->rch->l<=r)this->rch->Add(l,r,d);this->Maintain();}
}

其中判交部分写得非常无脑, 而且全程没有各种 \(\pm1\) 的烦恼.

注意第一行的 this->l/this->rl/r 是有区别的. this->l/this->r 指的是线段树所代表的"这个"区间, 而 l/r 则代表要修改的区间.

之前留下了一个野指针的问题. 显然每次调用的时候都保持查询区间和当前结点代表的区间有交集, 那么递归到叶子的时候依然有交集的话必然会覆盖整个结点(因为叶子结点只有一个点啊喂). 于是就可以保证代码不出问题.

使用

在主函数内可以这样使用:

int main(){Node* Tree=new Node(1,n);Tree->Add(l,r,d); // Add d to [l,r]
}

区间求和

按照线段树的分治套路, 我们只需要判断求和区间是否完全包含当前区间, 如果完全包含则直接返回, 否则下传惰性求值标记并分治下去, 对和求和区间相交的子树递归求和. 下面直接实现刚刚描述的分治过程.

int Query(int l,int r){if(l<=this->l&&this->r<=r)return this->sum;else{int ans=0;this->PushDown();if(l<=this->lch->r)ans+=this->lch->Query(l,r);if(this->rch->l<=r)ans+=this->rch->Query(l,r);return ans;}
}

其实在查询的时候, 有时候会维护一些特殊运算, 比如矩阵乘法/最大子段和一类的东西. 这个时候可能需要过一下脑子才能知道 ans 的初值是啥. 然而实际上我们直接用下面这种写法就可以避免临时变量与单位元初值的问题:

int Query(int l,int r){if(l<=this->l&&this->r<=r)return this->sum;else{this->PushDown();if(r<=this->lch->r)return this->lch->Query(l,r);if(this->rch->l<=l)return this->rch->Query(l,r);return this->lch->Query(l,r)+this->rch->Query(l,r);}
}

其中加法可以被改为任何满足结合律的运算.

主函数内可以这样使用:

int main(){Node* Tree=new Node(1,n);Tree->Add(l,r,d); // Add d to [l,r]printf("%d\n",Tree->Query(l,r)); // Query sum of [l,r]
}

可持久化

下面以进行单点修改区间求和并要求可持久化为例来说明.

先实现一个构造函数用来把原结点的信息复制过来:

Node(Node* ptr){*this=*ptr;
}

然后每次修改的时候先复制一遍结点就完事了. 简单无脑. (下面实现的是将下标为 \(x\) 的值改成 \(d\))

void Modify(int x,int d){if(this->l==this->r) //如果是叶子this->sum=d;else{if(x<=this->lch->r){this->lch=new Node(this->lch);this->lch->Modify(x,d);}else{this->rch=new Node(this->rch);this->rch->Modify(x,d);}this->Maintain();}
}

其实对于单点的情况还可以用问号表达式(或者三目运算符? 随便怎么叫了)搞一搞:

void Modify(int x,int d){if(this->l==this->r) //如果是叶子this->sum=d;else{(x<=this->lch->r?this->lch=new Node(this->lch):this->rch=new Node(this->rch))->Modify(x,d);this->Maintain();}
}

动态开点

动态开点的时候我们就不能随便留着野指针了. 因为我们需要通过判空指针来判断当前子树有没有被建立.

那么构造函数我们改成这样:

Node(int l,int r):l(l),r(r),add(0),sum(0),lch(NULL),rch(NULL){}

然后就需要注意处处判空了, 因为这次不能假定只要当前点不是叶子就可以安全访问子节点了.

遇到空结点如果要求和的话就忽略, 如果需要进入子树进行操作的话就新建.

而且在判断是否和子节点有交集的时候也不能直接引用子节点中的端点信息了, 有可能需要计算 int mid=(this->l+this->r)>>1. 一般查询的时候没有计算的必要, 因为发现结点为空之后不需要和它判交.

内存池

有时候动态分配内存可能会造成少许性能问题, 如果被轻微卡常可以尝试使用内存池.

内存池的意思就是一开始分配一大坨最后再用.

方法就是先开一块内存和一个尾指针, POOL_SIZE 为使用的最大结点数量:

Node Pool[POOL_SIZE]
Node* PTop=Pool;

然后将所有 new 替换为 new(PTop++) 就可以了. new(ptr) 的意思是对假装 ptr 指向的内存是新分配的, 然后调用构造函数并返回这个指针.

缺陷

显然这个写法也是有一定缺陷的, 目前发现的有如下几点:

  • 因为指针不能通过位运算快速得到LCA位置或 \(k\) 级祖先的位置于是跑得不如zkw线段树快.
  • 因为要在结点内存储左右端点所以内存开销相对比较大. 但是写完后可以通过将 this->l/this->r 替换为 thisl/thisr 再做少许修改作为参数传入即可缓解.
  • 看上去写得比较长. 但是实际上如果将函数写在结构体里面而不是事先声明, 并且将冗余的 this-> 去掉的话并没有长很多(毕竟参数传得少了啊喂).
  • 不能鲁棒处理 \(l>r\) 的情况. 因为递归的时候需要一直保证查询区间与当前区间有交集, 空集显然就GG了...

最后希望有兴趣的读者可以尝试实现一下这种写法, 万一发现这玩意确实挺好用呢?

(厚脸皮求推荐)

转载于:https://www.cnblogs.com/rvalue/p/11028820.html

[教程] 关于一种比较特别的线段树写法相关推荐

  1. 洛谷P4482 [BJWC2018]Border 的四种求法 字符串,SAM,线段树合并,线段树,树链剖分,DSU on Tree...

    原文链接https://www.cnblogs.com/zhouzhendong/p/LuoguP4482.html 题意 给定一个字符串 S,有 q 次询问,每次给定两个数 L,R ,求 S[L.. ...

  2. 牛客小白月赛 20 E区区区间(线段树)

    传送门 类似题:题目链接 题目: Keven 特别喜欢线段树,他给你一个长度为 n 的序列,对序列进行 m 次操作. 操作有两种: 1 l r k :表示将下标在 [l , r][l,r] 区间内的数 ...

  3. LeetCode Range Sum Query - Mutable(树状数组、线段树)

    问题:给出一个整数数组,求出数组从索引i到j范围内元素的总和.update(i,val)将下标i的数值更新为val 思路:第一种方式是直接根据定义,计算总和时直接计算从i到j的和 第二种方式是使用树状 ...

  4. Vijos P1103 校门外的树【线段树,模拟】

    校门外的树 描述 某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米.我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置:数轴上的每个整数点,即0,1,2,--, ...

  5. (转)线段树的区间更新

    原文地址:http://blog.csdn.net/zip_fan/article/details/46775633 写的很好,昨天刚刚开始写线段树,有些地方还不是很明白,看了这篇博文,学会了数组形式 ...

  6. 【算法微解读】浅谈线段树

    浅谈线段树 (来自TRTTG大佬的供图) 线段树个人理解和运用时,认为这个是一个比较实用的优化算法. 这个东西和区间树有点相似,是一棵二叉搜索树,也就是查找节点和节点所带值的一种算法. 使用线段树可以 ...

  7. 主席树——多棵线段树的集合

    主席树: (不要管名字) 我们有的时候,会遇到很多种情况,对于每一种情况,都需要通过线段树的操作实现. 碰巧的是,相邻两种情况下的线段树的差异不大.(总体的差异次数是O(N)级别的,均摊就是O(常数) ...

  8. Minimum Inversion Number HDU - 1394(权值线段树/树状数组)

    The inversion number of a given number sequence a1, a2, -, an is the number of pairs (ai, aj) that s ...

  9. hdu 5023 线段树染色问题

    题目链接 A Corrupt Mayor's Performance Art Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 100000/1 ...

  10. 【线段树】二进制(luogu 4428)

    正题 luogu 4428 题目大意 给你一个01串,让你进行一下两种操作: 1.将其中一位取反 2.问你某一段中有多少个子串满足有一种排列方案,使得组成的二进制数是3的倍数 解题思路 不难发现,因为 ...

最新文章

  1. 算法---最长湍流子数组
  2. 拼多多面试真题:如何用 Redis 统计独立用户访问量!
  3. [Android] 开发第二天
  4. C# 发送邮件的记录(qq,126,Gmail)
  5. ES6学习笔记(五):轻松了解ES6的内置扩展对象
  6. 高效分页存储过程代码
  7. MongoDB的查询语句示例说明
  8. postman发送json格式的post请求
  9. 《R语言初学指南》一1.2 向量
  10. C# 人民币大写金额转换
  11. 知网海外版(硕博论文pdf下载方式)
  12. 【实验室设备管理系统SSM】
  13. 验证手机号码是否正确
  14. Frenet坐标系与Cartesian坐标系互转(三):应用示例
  15. website for all kinds of courses
  16. 积木开发系列----Blockly初体验
  17. Mybatis中大于号和小于号表示方式
  18. DOS命令:systeminfo
  19. 关于Windows vivado综合卡死的问题
  20. kindeditor上传图片后自动缩放尺寸

热门文章

  1. xlsx表格怎么做汇总统计_办公软件excel表格数据统计-如何将多个EXCEL表格的数据进行汇总?...
  2. Multiples of 3 and 5
  3. js正则表达式判断非负数和是否为网址
  4. 苹果手机换电池对手机有影响吗_换手机不如换电池?手机电池影响手机寿命,这些知识早知道为好...
  5. 【报错】Verion 9 of Highlight.js has reached EOL
  6. 读中国通史的简注(周朝开始)
  7. 补能的争议路线:快充会走向大一统吗?
  8. 漫谈程序员系列:薪资,你是我不能言说的伤
  9. vant swipe 三图一屏
  10. 文青山在自动化测试空间的博客