MENU

1、建树(普通)

2、普通操作*4

3、差分思想*5

本文作者frankchenfu,blogs网址http://www.cnblogs.com/frankchenfu/,转载请保留此文字。

 线段树是所有数据结构中,最常用的之一。线段树的功能多样,既可以代替树状数组完成“区间和”查询,也可以完成一些所谓“动态RMQ”(可修改的区间最值问题)的操作。其中,它们大部分都是由递归实现的,因此就有一些问题——栈空间占用大和常数大

  因此,从中便衍生了一种非递归式的线段树(作者是THU的张昆玮,参见他自己的PPT讲稿《统计的力量-线段树),命名为zkw线段树。

  以下内容均用zkw线段树保存区间最大值作为演示。如果代码细节上有问题,请大家以自己写的为准,也欢迎向我反馈。

1、建树

  我们可以先观察左边面这张图。这张图本来是一张堆式的树形图,这里把它转化成了二进制。从中,我们可以发现最底层的节点舍去最低位,也就是说向右移一位之后,就变成了他们的父节点。同理,第二层中的结点也可以通过相同的方式变成根节点。

  因此,我们在构建这棵树时,就可以利用计算机的二进制建树,达到快速简单的目的。

 

 

  zkw线段树的操作几乎没有出现递归,而是用循环代替。例如建树操作(d数组存储数值):

  1. void build(int n)
  2. {
  3. for(bit=;bit<=n+;bit<<=);
  4. for(int i=bit+;i<=bit+n;i++)
  5. scanf("%d",&d[i]);
  6. for(int i=bit-;i;i--)
  7. d[i]=max(d[i<<],d[i<<|]);
  8. //i<<1|1 = (i<<1)+1 = 2*i+1
  9. }

(这里解释一下,bit表示非叶子节点,即倒二层及以上的节点数,每个节点保存的是它的值,如:和,最大值,最小值……)

  而普通的线段树建树则类似于(代码来自这里):

  1. struct SegTreeNode
  2. {
  3. int val;
  4. }segTree[MAXNUM];//定义线段树
  5.  
  6. void build(int root, int arr[], int istart, int iend)
  7. {
  8. if(istart == iend)//叶子节点
  9. segTree[root].val = arr[istart];
  10. else
  11. {
  12. int mid = (istart + iend) / ;
  13. build(root*+, arr, istart, mid);//递归构造左子树
  14. build(root*+, arr, mid+, iend);//递归构造右子树
  15. //根据左右子树根节点的值,更新当前根节点的值
  16. segTree[root].val = min(segTree[root*+].val, segTree[root*+].val);
  17. }
  18. }

  很简单的例子,说明了zkw线段树不仅不需要递归,而且在代码上也更简洁。

2、普通操作

  既然是线段树,那么就肯定能完成修改与查询操作

2.1 单点修改——二进制思想的运用

  单点修改也不难,他的思想就是先把叶节点修改,然后依次维护父节点(把所有和它有关的的修改掉)。例如这样:

  1. void update(int x,int y)
  2. {
  3. for(d[x+=bit]=y,x>>=;x;x>>=)
  4. d[x]=max(d[x<<],d[x<<|]);
  5. }

  这个代码就更为简短了(这里就不拿出来对比了)。

  当然,如果不是整个修改,而是加上或减去某数,只需要将for循环中的 d[x+=bit]=y 改为 d[x+=bit]+=y 即可(这里统一用整体修改作示范,下同)。

2.2 单点查询——最简单的查询

  假设数组中有 x 个元素,二叉树层数为 m,那么这 x 个元素在这个满二叉树中的编号就是$2^m$和$2^m+x-1$之间,即第x个元素就是$2^m+x-1$,访问起来很方便。

2.3 区间查询——单点查询的升级版

  区间查询也不难,规律同上,就是沿区间往上找。这里就直接上代码。

  1. int query(int s,int t)
  2. {
  3. int ans=-;
  4. for(s+=bit-,t+=bit+;s^t^;s>>=,t>>=)
  5. {
  6. if(~s&)
  7. ans=max(ans,d[s^]);
  8. if(t&)
  9. ans=max(ans,d[t^]);
  10. }
  11. return ans;
  12. }

2.4 区间修改——差分思想

  区间修改这时候看起来就很难办了……呃,怎么办呢??

  经过作者一个中午的实验,发现,用上述代码的思想似乎较难完成O($log_2$ n)级别的区间修改。这时候,翻开zkw神犇PPT讲稿,发现……原来,可以用差分的思想。(事实上,在普通线段树中,可以使用“懒标记”思想,不过限于作者水平,这里不再展开讨论)

3、差分思想

差分?

  差分是化绝对为相对的重要手段。我们接下来,数组里的d值就不在存最大值$d_n$了,而是另外开个数组m,存$m_n = d_n - d_{\frac{n}{2}} $,让每一个结点的值都是存他与他父亲结点的差值。

有什么用吗?

  当然有(不然说了干什么)!这时候,我们进行区间修改,就只需要修改$m_n$的值。

  这时候查询可以完成吗?可以。

  单点查询就是在m数组中,从要查的结点一直查到根结点,再加上d数组的值,就可以找到答案(这个应该很好理解吧)。

小插曲

  然后,我们在写代码的时候会发现,如果我们把d数组初始化为0的话,那么所有的修改都记在数组m中,d数组的值会变吗?不会。

  因此,我们干脆连值也不存了,把差分的“标记”直接当作值。于是,基本的差分思想就出来了。

  不过,值得一提的是,在常数上,差分的写法可能更大一些(不一定会明显优于递归版的普通线段树)。

3.1 差分思想与建树

这时候,每个点就像前面说的,存差就好了。代码如下,应该很好理解:

  1. void build(int n)
  2. {
  3. for(bit=;bit<=n+;bit<<=);
  4. for(int i=bit+;i<=bit+n;i++)
  5. scanf("%d",&d[i]);
  6. for(int i=bit-;i;i--)
  7. {
  8. d[i]=min(d[i<<],d[i<<|]);
  9. d[i<<]-=d[i];d[i<<|]-=d[i];
  10. }
  11. }

3.2 差分思想与单点修改

  你当然可以尝试区间修改,然后用像 query(,,x) 这样的方法修改。

不过完全没有这个必要。

  1. void update(int s,int t,int x)
  2. {
  3. int tmp;
  4. for(d[s]+=x;s>;s>>=)
  5. {
  6. tmp=max(d[s],d[s^]);d[s]-=tmp;d[s^]-=tmp;d[s>>]+=tmp;
  7. s>>=;
  8. }
  9. }

3.3差分思想与单点查询

  不得不承认,差分思想的运用,唯一一个不好的地方就是单点查询从O(1)变为了O($log_2$ n),但是他可以帮助我们完成区间修改的操作,因此也只好忍受一下了。

因为差分存储方式的运用,相应的,这时候的代码就成了这样:

  1. void query(int x)
  2. {
  3. int res=;
  4. while(x)
  5. res+=d[x],x>>=;
  6. return res;
  7. }

3.4差分思想与区间修改

就为了这个区间查询,我们几乎把内容翻了一倍——讲差分存储方式。而这种方式就是能够让我们完成区间修改。修改方式在上面介绍差分作用时提过了,这里就不在赘述了。代码:

  1. void update(int s,int t,int val)
  2. {
  3. s+=bit;t+=bit;int tmp;
  4. if(s==t)
  5. {
  6. for(d[s]+=val;s>;s>>=)
  7. {
  8. tmp=min(d[s],d[s^]);d[s]-=tmp;d[s^]-=tmp;d[s>>]+=tmp;
  9. }
  10. return;
  11. }
  12. for(d[s]+=val,d[t]+=val;s^t^;s>>=,t>>=)
  13. {
  14. if(~s&)d[s^]+ =val;
  15. if(t&) d[t^]+=val;
  16. tmp=min(d[s],d[s^]);d[s]-=tmp;d[s^]-=tmp;
  17. d[s>>]+=tmp;tmp=min(d[t],d[t^]);
  18. d[t]-=tmp;d[t^]-=tmp;d[t>>]+=tmp;
  19. }
  20. for(;s>;s>>=)
  21. {
  22. tmp=min(d[s],d[s^]);d[s]-=tmp;d[s^]-=tmp;
         d[s>>]+=tmp;
  23. }
  24. return;
  25. }

3.5差分思想与区间查询

区间查询?其实和之前没用差分的差不多,只是把它求出来之后,再把值依层还原回去。

  1. int query(int s,int t)
  2. {
  3. int lans=,rans=,ans;
  4. if(s==t)
  5. {
  6. for(s+=bit;s;s>>=)
  7. lans+=d[s];
  8. return lans;
  9. }
  10. for(s+=bit,t+=bit;s^t^;s>>=,t>>=)
  11. {
  12. lans+=d[s];rans+=d[t];
  13. if(~s&) lans=min(lans,d[s^]);
  14. if(t&) rans=min(rans,d[t^]);
  15. }
  16. lans+=d[s];rans+=d[t];
  17. for(ans=min(lans,rans);s>;)
  18. ans+=d[s>>=1];
  19. return ans;
  20. }

至此,zkw线段树的基本操作到这里就讲完了。让我们回顾一下,zkw线段树的优点不仅在于常数小,空间小(对于一般情况下的写法),而且好写好调,是一种优秀的数据结构。它的本质是非递归式线段树。希望这篇博客的内容对大家有帮助,满意请在右下方点个赞,谢谢。

数据结构3——浅谈zkw线段树的更多相关文章

  1. 浅谈算法——线段树之Lazy标记

    一.前言 前面我们已经知道线段树能够进行单点修改和区间查询操作(基本线段树).那么如果需要修改的是一个区间该怎么办呢?如果是暴力修改到叶子节点,复杂度即为\(O(nlog n)\),显然是十分不优秀的 ...

  2. zkw 线段树

    优秀的 zkw 线段树讲解:<线段树的扩展之浅谈 zkw 线段树> 存一份模板代码(区间修改.区间查询): /* zkw Segment Tree * Au: GG */ #include ...

  3. ZKW线段树

    简介 zkw线段树虽然是线段树的另一种写法,但是本质上已经和普通的递归版线段树不一样了,是一种介于树状数组和线段树中间的存在,一些功能上的实现比树状数组多,而且比线段树好写且常数小. 普通线段树采用从 ...

  4. 【POJ3468】【zkw线段树】A Simple Problem with Integers

    Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. On ...

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

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

  6. 『zkw线段树及其简单运用』

    阅读本文前,请确保已经阅读并理解了如下两篇文章: 『线段树 Segment Tree』 『线段树简单运用』 引入 这是一种由\(THU-zkw\)大佬发明的数据结构,本质上是经典的线段树区间划分思想, ...

  7. 普及向 ZKW线段树!

    啊,是否疲倦了现在的线段树 太弱,还递归! 那我们就欢乐的学习另外一种神奇的线段树吧!(雾 他叫做zkw线段树   这个数据结构灰常好写(虽然线段树本身也特别好写……) 速度快(貌似只在单点更新方面比 ...

  8. 【bzoj3685】普通van Emde Boas树 权值zkw线段树

    原文地址:http://www.cnblogs.com/GXZlegend/p/6809743.html 题目描述 设计数据结构支持:1 x  若x不存在,插入x2 x  若x存在,删除x3    输 ...

  9. dijkstra之zkw线段树优化

    其实特别好理解,我们只要写一个数据结构(线段树)支持一下操作: 1.插入一个数\(x\). 2.查询当前数据结构中最小的数的插入编号. 3.删除插入编号为\(x\)的数. 第一眼看成可持久化了 其实就 ...

随机推荐

  1. Swift 入门之简单语法(三)

    集合 数组 数组使用 [] 定义,这一点与 OC 相同 //: [Int] let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 遍历 for num in nu ...

  2. URL的标准格式

    URL的标准格式 scheme://host:port/path?query#fragment 1.  scheme:协议 2. host:主机 3. port:端口 4. path:路径 5. qu ...

  3. 在vue-cli搭建的项目中增加后台mock接口

    用vue-cli搭建一个前端开发环境确实是极其方便,在写前端代码肯定也是少不了需要调用后台提供的业务接口进行前后端交互,特别在敏捷开发中,前后端都要提前确定业务接口并进行打桩,在开发过程中基本是没有现 ...

  4. python 基础篇 2

    三.对变量.对象与赋值的浅析 1.不记住就完蛋了 1.1 记住:一切数据都是对象 1.2 记住:一切变量都是对数据对象的一个引用 1.3 分析:python内部的引用计数 sys.getrefcoun ...

  5. VR全景智慧城市,开启“上帝视角”体验‘身临其境’

    VR全景把不同的场景 分为若干个VR视角点 进入一个视角点,用户便能开启"上帝视角" 转动手机,身临其境地360度转动察看,对场景的全貌和细节一目了然.   人生处处有尴尬 比如大 ...

  6. 将数据的初始化放到docker中的整个工作过程(问题记录)

    将数据的初始化放到docker中的整个工作过程 由于是打算作为个人博客,所以对于install这个步骤,我从一开始就打算删掉的,前面一个多星期一直在修bug,到前天才开始做这个事情. 过程中也是碰到了 ...

  7. Python使用PyMysql操作数据库

    安装 pip install -U pymysql 连接数据库 连接数据库有两种不同的格式 直接使用参数 代码如下 import pymysql.cursors connection = pymysq ...

  8. Codility---EquiLeader

    Task description A non-empty zero-indexed array A consisting of N integers is given. The leader of t ...

  9. vue 高德地图之玩转周边

    前言:在之前的博客中,有成功引入高德地图,这是以前的地址  vue 调用高德地图. 因为一些需求,需要使用到地图的周边功能. 完整的项目代码请查看  我的github 一 .先看要实现的结果,参考了链 ...

  10. Linux crontab定时执行任务命令格式与详细例子

    基本格式 : * * * * * command 分 时 日 月 周 命令 第1列表示分钟1-59 每分钟用*或者 */1表示 第2列表示小时1-23(0表示0点) 第3列表示日期1-31 第4列表示 ...