(转载)浅谈线段树

浅谈线段树

                                                                         数据结构——线段树

O、引例

A.给出n个数,n<=100,和m个询问,每次询问区间[l,r]的和,并输出。

一种回答:这也太简单了,O(n)枚举搜索就行了。

另一种回答:还用得着o(n)枚举,前缀和o(1)就搞定。

那好,我再修改一下题目。

B.给出n个数,n<=100,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。

回答:o(n)枚举。

动态修改最起码不能用静态的前缀和做了。

好,我再修改题目:

C.给出n个数,n<=1000000,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。

回答:o(n)枚举绝对超时。

再改:

D,给出n个数,n<=1000000,和m个操作,每个操作修改一段连续区间[a,b]的值

回答:从a枚举到b,一个一个改。。。。。。有点儿常识的人都知道超时

那怎么办?这就需要一种强大的数据结构:线段树。

一、基本概念

1、线段树是一棵二叉搜索树,它储存的是一个区间的信息。

2、每个节点以结构体的方式存储,结构体包含以下几个信息:

     区间左端点、右端点;(这两者必有)

     这个区间要维护的信息(事实际情况而定,数目不等)。

3、线段树的基本思想:二分

4、线段树一般结构如图所示:

5、特殊性质:

由上图可得,

1、每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]

2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质

二、线段树的基础操作

注:以下基础操作均以引例中的求和为例,结构体以此为例:

struct node
{
       int l,r,w;//l,r分别表示区间左右端点,w表示区间和
}tree[4*n+1];

线段树的基础操作主要有5个:

建树、单点查询、单点修改、区间查询、区间修改。

1、建树,即建立一棵线段树

   ① 主体思路:a、对于二分到的每一个结点,给它的左右端点确定范围。

                     b、如果是叶子节点,存储要维护的信息。

                     c、状态合并。

  ②代码

复制代码
void build(int l,int r,int k)
{tree[k].l=l;tree[k].r=r;if(l==r)//叶子节点 {scanf("%d",&tree[k].w);return ; }int m=(l+r)/2;build(l,m,k*2);//左孩子 build(m+1,r,k*2+1);//右孩子 tree[k].w=tree[k*2].w+tree[k*2+1].w;//状态合并,此结点的w=两个孩子的w之和 
}
复制代码

③注意

 a.结构体要开4倍空间,为啥自己画一个[1,10]的线段树就懂了

 b.千万不要漏了return语句,因为到了叶子节点不需要再继续递归了。

2、单点查询,即查询一个点的状态,设待查询点为x

   ①主体思路:与二分查询法基本一致,如果当前枚举的点左右端点相等,即叶子节点,就是目标节点。如果不是,因为这是二分法,所以设查询位置为x,当前结点区间范围为了l,r,中点为         mid,则如果x<=mid,则递归它的左孩子,否则递归它的右孩子

   ②代码

复制代码
void ask(int k)
{if(tree[k].l==tree[k].r) //当前结点的左右端点相等,是叶子节点,是最终答案 {ans=tree[k].w;return ;}int m=(tree[k].l+tree[k].r)/2;if(x<=m) ask(k*2);//目标位置比中点靠左,就递归左孩子 else ask(k*2+1);//反之,递归右孩子 
}
复制代码

  ③正确性分析:

     因为如果不是目标位置,由if—else语句对目标位置定位,逐步缩小目标范围,最后一定能只到达目标叶子节点。

3、单点修改,即更改某一个点的状态。用引例中的例子,对第x个数加上y

①主体思路

 结合单点查询的原理,找到x的位置;根据建树状态合并的原理,修改每个结点的状态。

 ②代码

复制代码
void add(int k)
{if(tree[k].l==tree[k].r)//找到目标位置 {tree[k].w+=y;return;}int m=(tree[k].l+tree[k].r)/2;if(x<=m) add(k*2);else add(k*2+1);tree[k].w=tree[k*2].w+tree[k*2+1].w;//所有包含结点k的结点状态更新 
}
复制代码

4、区间查询,即查询一段区间的状态,在引例中为查询区间[x,y]的和

①主体思路

 

 

mid=(l+r)/2

y<=mid ,即 查询区间全在,当前区间的左子区间,往左孩子走

x>mid 即 查询区间全在,当前区间的右子区间,往右孩子走

否则,两个子区间都走

②代码

复制代码
void sum(int k)
{if(tree[k].l>=x&&tree[k].r<=y) {ans+=tree[k].w;return;}int m=(tree[k].l+tree[k].r)/2;if(x<=m) sum(k*2);if(y>m) sum(k*2+1);
}
复制代码

③正确性分析

情况1,3不用说,对于情况2,最差情况是搜到叶子节点,此时一定满足情况1

5、区间修改,即修改一段连续区间的值,我们已给区间[a,b]的每个数都加x为例讲解

    Ⅰ.引子

 

       有人可能就想到了:

       修改的时候只修改对查询有用的点。

       对,这就是区间修改的关键思路。

      为了实现这个,我们引入一个新的状态——懒标记

  Ⅱ 懒标记

     (懒标记比较难理解,我尽力讲明白。。。。。。)

       1、直观理解:“懒”标记,懒嘛!用到它才动,不用它就睡觉。

       2、作用:存储到这个节点的修改信息,暂时不把修改信息传到子节点。就像家长扣零花钱,你用的时候才给你,不用不给你。

       3、实现思路(重点):

           a.原结构体中增加新的变量,存储这个懒标记。

           b.递归到这个节点时,只更新这个节点的状态,并把当前的更改值累积到标记中。注意是累积,可以这样理解:过年,很多个亲戚都给你压岁钱,但你暂时不用,所以都被你父母扣下了。

           c.什么时候才用到这个懒标记?当需要递归这个节点的子节点时,标记下传给子节点。这里不必管用哪个子节点,两个都传下去。就像你如果还有妹妹,父母给你们零花钱时总不能偏心吧

           d.下传操作:

               3部分:①当前节点的懒标记累积到子节点的懒标记中。

                         ②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记

                            这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?

                            因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积

                         ③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。就像你父母给了你5元钱,你不能说因为前几次给了你10元钱, 所以这次给了你15元,那你不就亏大了。 

     懒标记下穿代码:f为懒标记,其余变量与前面含义一致。

复制代码
void down(int k)
{tree[k*2].f+=tree[k].f;tree[k*2+1].f+=tree[k].f;tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);tree[k].f=0;
}
复制代码

 Ⅲ 完整的区间修改代码:

复制代码
void add(int k)
{if(tree[k].l>=a&&tree[k].r<=b)//当前区间全部对要修改的区间有用 {tree[k].w+=(tree[k].r-tree[k].l+1)*x;//(r-1)+1区间点的总数tree[k].f+=x;return;}if(tree[k].f) down(k);//懒标记下传。只有不满足上面的if条件才执行,所以一定会用到当前节点的子节点 int m=(tree[k].l+tree[k].r)/2;if(a<=m) add(k*2);if(b>m) add(k*2+1);tree[k].w=tree[k*2].w+tree[k*2+1].w;//更改区间状态 
}
复制代码

 Ⅳ.懒标记的引入对其他基本操作的影响

     因为引入了懒标记,很多用不着的更改状态存了起来,这就会对区间查询、单点查询造成一定的影响。

     所以在使用了懒标记的程序中,单点查询、区间查询也要像区间修改那样,对用得到的懒标记下传。其实就是加上一句if(tree[k].f)  down(k),其余不变。

     2017.5.16 之前写的单点修改不需要下传懒标记,在此订正:单点修改也需要下传懒标记

     引入了懒标记的单点查询代码:

复制代码
 void ask(int k)//单点查询
{if(tree[k].l==tree[k].r){ans=tree[k].w;return ;}if(tree[k].f) down(k);//懒标记下传,唯一需要更改的地方int m=(tree[k].l+tree[k].r)/2;if(x<=m) ask(k*2);else ask(k*2+1);
}
复制代码

    引入了懒标记的区间查询代码:

复制代码
void sum(int k)//区间查询
{if(tree[k].l>=x&&tree[k].r<=y) {ans+=tree[k].w;return;}if(tree[k].f)  down(k)//懒标记下传,唯一需要更改的地方int m=(tree[k].l+tree[k].r)/2;if(x<=m) sum(k*2);if(y>m) sum(k*2+1);
}
复制代码

三、总结

线段树5种基本操作代码:

复制代码
#include<cstdio>
using namespace std;
int n,p,a,b,m,x,y,ans;
struct node
{int l,r,w,f;
}tree[400001];
inline void build(int k,int ll,int rr)//建树 
{tree[k].l=ll,tree[k].r=rr;if(tree[k].l==tree[k].r){scanf("%d",&tree[k].w);return;}int m=(ll+rr)/2;build(k*2,ll,m);build(k*2+1,m+1,rr);tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
inline void down(int k)//标记下传 
{tree[k*2].f+=tree[k].f;tree[k*2+1].f+=tree[k].f;tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);tree[k].f=0;
}
inline void ask_point(int k)//单点查询
{if(tree[k].l==tree[k].r){ans=tree[k].w;return ;}if(tree[k].f) down(k);int m=(tree[k].l+tree[k].r)/2;if(x<=m) ask_point(k*2);else ask_point(k*2+1);
}
inline void change_point(int k)//单点修改 
{if(tree[k].l==tree[k].r){tree[k].w+=y;return;}if(tree[k].f) down(k);int m=(tree[k].l+tree[k].r)/2;if(x<=m) change_point(k*2);else change_point(k*2+1);tree[k].w=tree[k*2].w+tree[k*2+1].w; 
}
inline void ask_interval(int k)//区间查询 
{if(tree[k].l>=a&&tree[k].r<=b) {ans+=tree[k].w;return;}if(tree[k].f) down(k);int m=(tree[k].l+tree[k].r)/2;if(a<=m) ask_interval(k*2);if(b>m) ask_interval(k*2+1);
}
inline void change_interval(int k)//区间修改 
{if(tree[k].l>=a&&tree[k].r<=b){tree[k].w+=(tree[k].r-tree[k].l+1)*y;tree[k].f+=y;return;}if(tree[k].f) down(k);int m=(tree[k].l+tree[k].r)/2;if(a<=m) change_interval(k*2);if(b>m) change_interval(k*2+1);tree[k].w=tree[k*2].w+tree[k*2+1].w;
}
int main()
{scanf("%d",&n);//n个节点 build(1,1,n);//建树 scanf("%d",&m);//m种操作 for(int i=1;i<=m;i++){scanf("%d",&p);ans=0;if(p==1){scanf("%d",&x);ask_point(1);//单点查询,输出第x个数 printf("%d",ans);} else if(p==2){scanf("%d%d",&x,&y);change_point(1);//单点修改 }else if(p==3){scanf("%d%d",&a,&b);//区间查询 ask_interval(1);printf("%d\n",ans);}else{scanf("%d%d%d",&a,&b,&y);//区间修改 change_interval(1);}}
}
复制代码

 

 四、空间优化

父节点k,左二子2*k,右儿子2*k+1,需要4*n的空间

但并不是所有的叶子节点占用到2n+1——4n

这就造成大量空间浪费

2*n空间表示法:推荐博客:http://www.cppblog.com/MatoNo1/archive/2015/05/05/195857.html

用dfs序表示做节点下标

父节点k,左儿子k+1,右儿子:k+左儿子区间长度*2,不是父节点下标+父节点区间长度。因为当树不满时,两者不相等

具体实现这里就不再写模板了,就是改改左右儿子的下标

可参考代码: 题目:楼房重建http://www.cnblogs.com/TheRoadToTheGold/p/6361242.html 

 

里面的建树用的2*n空间

五、模板题

1、codevs 1080 线段树练习 (单点修改+区间查询)  http://codevs.cn/problem/1080/  

 View Code

2、codevs 1081 线段树练习2 (单点查询+区间修改) http://codevs.cn/problem/1081/

 View Code

3、codevs 1082 线段树练习3  (区间修改+区间查询)

 View Code

六、经典例题

> codevs 3981/SPOJ GSS1/GSS3 ——区间最大子段和
> Bzoj3813 奇数国——区间内某个值是否出现过
>洛谷 P2894 酒店 Hotel ——区间连续一段空的长度
> codevs 2421 /Bzoj1858 序列操作——多种操作
> codevs 2000 / BZOJ 2957: 楼房重建——区间的最长上升子序列
 Codevs3044 矩形面积求并——扫描线

 

作者:xxy
出处:http://www.cnblogs.com/TheRoadToTheGold/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

转载于:https://www.cnblogs.com/rmy020718/p/8832889.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/353788.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

双显示器设置:如何设置一台电脑两个显示器

双显示器设置&#xff1a;如何设置一台电脑两个显示器 -来源&#xff1a;互联网 作者&#xff1a;佚名 时间&#xff1a;04-11 09:00:18 【大 中 小】 点评&#xff1a;双显示器设置,如何设置一台电脑两个显示器&#xff1a;一般来说一台电脑通常只配一个显示器&#xff0c;在我…

vue element-ui 的奇怪组件el-switch

https://segmentfault.com/q/1010000010008343转载于:https://www.cnblogs.com/Chenshuai7/p/8847917.html

单元测试怎么测试线程_单元测试线程代码的5个技巧

单元测试怎么测试线程以下是一些技巧&#xff0c;说明如何进行代码的逻辑正确性测试&#xff08;与多线程正确性相对&#xff09;。 我发现本质上有两种带有线程代码的刻板印象模式&#xff1a; 面向任务-许多短期运行的同类任务&#xff0c;通常在Java 5执行程序框架内运行&a…

UBUNTU下双显示器设置

UBUNTU下双显示器设置 (2010-05-08 17:31) 分类&#xff1a; linux ubuntu&#xff08;GNOME&#xff09;现在已经能很好的处理双屏了&#xff0c;无论是克隆方式还是扩展方式&#xff01;   但有时我们需要一个不同的管理器如awesome、fluxbox这类简单的窗口管理器中又如何设…

结对第二次作业

题目要求 我们在刚开始上课的时候介绍过一个小学四则运算自动生成程序的例子&#xff0c;请实现它&#xff0c;要求&#xff1a; 能够自动生成四则运算练习题可以定制题目数量用户可以选择运算符用户设置最大数&#xff08;如十以内、百以内等&#xff09;用户选择是否有括号、…

JavaFX缺少的功能调查:CSS

在“ 缺少的功能调查”系列的最后一篇文章中&#xff0c;我说过这篇文章是关于CSS和FXML中缺少的功能。 现在事实证明&#xff0c;调查提交的内容不包含任何有效的FXML问题。 因此&#xff0c;我将仅关注CSS。 这些是报告CSS功能丢失&#xff1a; 完全CSS支持–当前JavaFX CS…

JAVA程序员面试题集合

JAVA程序员面试题集合 分类&#xff1a; 编程语言 2012-12-08 12:10 50人阅读 评论(0) 收藏 举报 1&#xff0e;面向对象的特征有哪些方面(1)抽象&#xff1a;抽象就是忽略一个主题中与当前目标无关的那些方面&#xff0c;以便更充分地注意与当前目标有关的方面。抽象并不打算…

STM32F105 USB管脚Vbus的处理

源&#xff1a;STM32F105 USB管脚Vbus的处理 对于STM32F105/107来说&#xff0c;为了监测USB的连接问题&#xff0c;程序默认是通过Vbus管脚进行检查的。但是Vbus管脚和UART1的TXD复用&#xff0c;导致我们在使用UART1发送数据时候&#xff0c;USB重启的问题。为了解决这个问题…

Spy++原理初探

Spy原理初探 http://www.vckbase.com/index.php/wv/1480.html文章概要&#xff1a;用Visual Studio搞开发的朋友对Spy这个工具一定不陌生&#xff0c;它可以分析窗体结构、进程和窗口消息&#xff0c;对开发工作有很大辅助作用。我们需要研究某个对象时&#xff0c;只要调出其…

gradle ant_使用Gradle引导旧式Ant构建

gradle antGradle提供了几种不同的方式来利用您在Ant上的现有投资&#xff0c;无论是积累的知识还是您已经放入构建文件的时间。 这可以极大地方便将Ant生成的项目移植到Gradle的过程&#xff0c;并为您提供逐步进行此操作的路径。 Gradle文档在描述如何在Gradle构建脚本中使用…

confluence 为合并的单元格新增一行

1&#xff0c;先将最后一个结构取消合并单元格 | | ___ | | | ___ | | _ | ___ | 2&#xff0c;在最后一行追加一行&#xff0c;将左侧合并 3&#xff0c;将上面取消合并的重新合并即可转载于:https://www.cnblogs.com/lavin/p/8866867.html

java怎么把system.out的东西输出到文件上

java怎么把system.out的东西输出到文件上 浏览(1836)|评论(0) 交流分类&#xff1a;Java|笔记分类: 未分类 RT&#xff0c;我们在程序里system.out的东西都是从控制台刷过。 如果你想它写到文件里&#xff0c;以下是一种解决方法&#xff0c;当然你还可以用log4j java代码…

纯Java中的Functor和Monad示例

本文最初是我们使用RxJava进行反应式编程的附录。 但是&#xff0c;尽管与反应式编程非常相关&#xff0c;但对monad的介绍却不太适合。 因此&#xff0c;我决定将其取出并作为博客文章单独发布。 我知道&#xff0c;“ 我自己的&#xff0c;对单子的一半正确和半完全的解释 ”…

[NOI2012]美食节

题解&#xff1a; 很经典的网络流 对于每个厨师拆点分开统计 1倍 2倍 3倍 n&#xff08;mp)^2 有点大 动态加边 即对于每个厨师有了i才会有i1 不过好像还是有点卡常&#xff1f;&#xff1f; 代码&#xff1a; #include <bits/stdc.h> using namespace std; #define INF …

请问:如何实现文件日志功能?要求每天换一个文件。文件名以日期区分

结帖率&#xff1a;100%#1 得分&#xff1a;0 回复于&#xff1a; 2004-06-22 17:24:13 使用一个单独的类封装日志记录&#xff0c;在该类中记录当前使用的文件名&#xff0c;每次记日志时获取一下系统时间&#xff0c;与文件名匹配一下&#xff0c;发现日期不同则新开文件。下…

微信小程序的scroll-view组件

scroll-view为滚动视图&#xff0c;共有水平滚动和垂直滚动两种使用竖向滚动时&#xff0c;需要给<scroll-view/>一个固定高度&#xff0c;通过 WXSS 设置 height。index.wxss 是页面的结构文件&#xff1a;<!--垂直滚动--> <view class"section">…

eclipse使用技巧_有效使用Eclipse的热门技巧

eclipse使用技巧以下是一些技巧&#xff0c;可以帮助您避免潜在的问题并在使用Eclipse时提高工作效率。 避免安装问题 切勿在旧版本之上安装新版本的Eclipse。 首先重命名旧版本&#xff0c;以将其移开&#xff0c;然后将新版本解压缩到干净的目录中。 恢复混乱的工作空间 …

日志文件的编写

/// <summary> /// 写入日志到文本文件 /// </summary> /// <param name"action">动作</param> /// <param name"strMessage">日志内容</param> /// <param name"time">时间</param> pub…

MFC非模态对话框实例

【转载】MFC非模态对话框实例 2012-06-17 16:21:41| 分类&#xff1a; C/MFC | 标签&#xff1a; |字号大中小 订阅 实例目的 在Windows环境中&#xff0c;对话框是一种常用的输入输出手段。对话框有两种类型&#xff0c;非模态和模态。非模态对话框与模态对话框不同&a…

Node简单服务器开发

运用的知识&#xff1a;http&#xff0c;fs&#xff0c;get&#xff0c;post 接口定义&#xff1a;/user?actreg$useraaa&passbbb后台返回格式&#xff1a;{"ok":false,"msg":"原因"}/user?actlogin$useraaa&passbbb后台返回格式&…