线段树是一种二叉搜索树,它的每一个结点对应着一个区间[L, R],叶子结点对应的区间就是一个单位区间,即L == R。对于一个非叶子结点[L, R],它的左儿子所表示的区间是[L, (L +R)/2],右儿子所代表的的区间是[(L + R) / 2 +1, R]。

  拿一个简单的例子来说,我们需要维护一个数列,每次进行以下两种操作:

  • 修改一个元素
  • 查询一段区间的最大值

这是一道经典的RMQ(range minimum/maximum query,区间最值查询问题)问题,用线段树怎么解决呢?更新是点更新,查询是区间查询。

具体操作如下:

  建树的时候,始终遵循每个结点维护结点所代表的左右端点和该区间的最值,建树的时候如果到叶子结点,那么这个结点的最值就是对应位置的数列的值,否则递归的建立左子树和右子树,然后将当前结点的区间最值设置为自己左子树和右子树最值的较大值。

  先定义线段树的结点:

const int maxn = ;

struct Node {
int l, r, mx;//左右区间端点和最大值
}tr[maxn<<];

建树:

 void build(int d, int l, int r) {//递归建树
tr[d].l = l, tr[d].r = r;
if(l == r) { //叶子结点
tr[d].mx = b[l];
return;
}
int mid = (l + r) / ;
int lc = d * ;
int rc = d * + ;
build(lc, l ,mid); //递归构建左子树
build(rc, mid + , r); //递归构建右子树
tr[d].mx = max(tr[lc].mx, tr[rc].mx);//该区间的最值是左子结点和右子结点的最大值
}

  如果是查询操作,从根结点开始查询:如果查询区间在该结点的左子内,则查询左子;如果查询区间在该结点的右子树内,则查询右子树;否则,查询左子树相应区间和右子树相应区间,并将两者的返回值的较大值返回。代码如下:

 int query(int d, int l, int r) {
if(tr[d].l == l && tr[d].r == r) {//待查询区间等于当前结点的区间范围
return tr[d].mx;
}
int mid = (tr[d].l + tr[d].r) / ;//取当前结点的中点
int lc = d * ;
int rc = d * + ;
if(r <= mid) return query(lc, l, mid);//查询区间属于当前结点的左子树就查询左子树
else if(l > mid) return query(rc, mid + , r);//查询区间属于当前结点的右子树就查询右子树
else return max(query(lc, l, mid), query(rc, mid + , r));//查询区间分布在两侧
}

  如果是修改操作,则从根结点开始修改,一直修改到叶子结点,同时对路径上相应结点的最值进行更新。

 void modify(int d, int pos, int v) {//将位置为pos的元素改成v
if(tr[d].l == tr[d].r && tr[d].l == pos) {//如果当前结点是叶子结点且是该结点
tr[d].mx = v;
return;
}
int mid = (tr[d].l + tr[d].r) / ;
int lc = d * ;
int rc = d * + ;
if(pos <= mid) modify(lc, pos, v);//如果要修改的位置在当前结点的左子树
else modify(rc, pos, v); // 右子树
tr[d].mx = max(tr[lc].mx, tr[rc].mx);
}

  以上是点更新加上区间查询,运用的时候将元素存入数组b中,建树,直接修改、查询即可。

  明白了基本的原理,下面介绍一种实现起来更简短,使用更方便的写法。

 const int INF = ;
int ql, qr;//查询区间
int query(int o, int L, int R) {
int M = L + (R - L) / ;
int ans = -INF;
if(ql <= L && R <= qr) return maxv[o]; //当前结点完全包含在查询区间内
if(ql <= M) ans = max(ans, query(o * , L, M)); //往左走
if(M < qr) ans = max(ans, query(o * + , M + , R));//往右走
return ans;
} int p, v;//修改A[p] = v
void update(int o, int L, int R) {
int M = L + (R - L) / ;
if(L == R) maxv[o] = v;
else {
if(p <= M) update(o * , L, M);
else update(o * + , M + , R);
maxv[o] = max(maxv[o * ], maxv[o * + ]);
}
}

  使用的时候建树的过程是每次读入一个数,使用update函数更新A[i] = x。然后直接查询、修改即可。

  以上是点更新加上区间查询,如果没有点更新,只是查询某个区间的最值,则直接使用ST算法(简单不易写错)。

  但是通常在题目中会遇到对区间进行更新的操作,比如给出一个n个元素的数组A1,A2,A3...An,你的任务是设计一个数据结构,支持一下两种操作。

  • Add(L,R,v):把AL,AL+1,...,AR的值全部增加v。
  • Query(L,R):计算子序列AL,AL,...AR的元素和、最小值和最大值。

  我们需要在线段树中维护3个信息sum,min,max,分别对应三个查询值。其中如果还是使用sum[o]表示“结点o对应区间中所有数之和”,则add操作最坏情况下会修改所有的sum。解决的办法是把sum[o]的定义改成“如果只执行结点o及其子孙结点中的add操作,结点o对应区间中所有数之和”。信息维护的代码如下:

 //维护结点o,对应区间[L,R]
void maintain(int o, int L, int R) {
int lc = o * ;
int rc = O * + ;
sumv[o] = minv[o] = maxv[o] = ;
if(R > L) {//考虑左右子树
sumv[o] = sumv[lc] + sumv[rc];
minv[o] = min(minv[lc], minv[rc]);
maxv[o] = max(maxv[lc], maxv[rc]);
}
minv[o] += addv[o];
maxv[o] += addv[o];
sumv[o] += addv[o] * (R - L + );
}

  上述维护结点o的maintain函数在递归访问到的结点都需要调用,并且在递归返回后调用。代码如下:

 //其中y1,y2表示修改和查询的区间
void update(int o, int L, int R) {
int lc = o * ;
int rc = o * + ;
if(y1 <= L && y2 >= R) {//递归边界
addv[o] += v;
} else {
int M = L + (R - L) / ;
if(y1 <= M) update(lc, L, M);
if(y2 > M) update(rc, M + , R);
}
maintain(o, L, R);//递归结束后重新计算本结点附加信息
}

  接下来就是查询操作了,基本思路仍然是把查询区间递归分解为若干不相交子区间,把各个子区间的查询结果加以合并,但是需要注意的是每个边界区间的结果不能直接使用,还得考虑祖先结点对它的影响。为了方便,我们在递归查询函数中增加了一个参数,表示当前区间的所有祖先结点add值之和。代码如下:

 int _min, _max, _sum;//对应查询结果
void query(int o, int L, int R, int add) {
if(y1 <= L && y2 >= R) {
_sum += sumv[o] + add * (R - L + );
_min = min(_min, minv[o] + add);
_max = max(_max, maxv[o] + add);
} else {//递归统计累加参数add
int M = L + (R - L) / ;
if(y1 <= M) query(o * , L, M, add + addv[o]);
if(y2 > M) query(o * + , M + , R, add + addv[o]);
}
}

  上述讲解的是区间增减,还有一种情况是区间赋值。即给出一个有n个元素的数组,A1,A2,...,An,你的任务是设计一个数据结构,支持一下两种操作:

  • Set(L, R, v):把AL,AL+1,...AR的值全部修改成v(v>=0)
  • Query(L,R):计算子序列AL,AL,...AR的元素和、最小值和最大值。

  同理我们将set操作也进行分解,记录在结点中,但是出现了一个新的问题,即add操作没有先后的时效性,但是set操作是有的。

  解决的办法是设计一个向下传递函数,用来做一个标记。

  新的修改操作代码如下:

 void update(int o, int L, int R) {
int lc = o * ;
int rc = o * + ;
if(y1 <= L && y2 >= R) {//递归边界,将set标记修改
setv[o] = v;
} else {
pushdown(o);
int M = L + (R - L) / ;
if(y1 <= M) update(lc, L, M); else maintain(lc, L, M);
if(y2 > M) update(rc, M + , R); else maintain(rc, M + , R);
}
maintain(o, L, R);//递归结束后重新计算本结点附加信息
}

其中需要注意的有两个地方,首先是pushdown函数,它的作用就是把set值往下传递。

 void pushdown(int o) {
int lc = o * ;
int rc = o * + ;
if(setv[o] >= ) {//由于赋的值是大于等于0的,所以>= 0表示有标记
setv[lc] = setv[rc] = setv[o];
setv[o] = -; //清除标记
}
}

  另一个值得注意的地方是代码出多了两处maintain的调用。对于本来就要递归访问的子树,递归访问结束之后自然会调用maintain,因此只需要针对不进行递归访问的子树调用maintain即可。

  接下来就是关键的查询问题了,怎么解决任意两个set操作不会存在祖先-后代关系的问题。

  其实我们只需规定在这种情况下,以祖先结点上的操作为准即可,在递归查询的时候,碰到到一个set操作就立即停止即可。代码如下:

 void query(int o, int L, int R) {
if(setv[o] >= ) { //递归边界1:有set标记
_sum += setv[o] * (min(R, y2) - max(L, y1) + );
_min = min(_min, setv[o]);
_max = max(_max, setv[o]);
} else if(y1 <= L && y2 >= R) {//递归边界2:边界区间
_sum += sumv[o]; //此区间没有被任何set操作影响
_min = min(_min, minv[o]);
_max = max(_max, maxv[o]);
} else { //递归统计
int M = L + (R - L) / ;
if(y1 <= M) query(o * , L, M);
if(y2 > M) query(o * + , M + , R);
}
}

  暂时线段树的讲解就到这里,理解的还不是太透彻,之后会补上几道例题。

  

线段树(segment tree)的更多相关文章

  1. 『线段树 Segment Tree』

    更新了基础部分 更新了\(lazytag\)标记的讲解 线段树 Segment Tree 今天来讲一下经典的线段树. 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间 ...

  2. 线段树(Segment Tree)(转)

    原文链接:线段树(Segment Tree) 1.概述 线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,基本能保证每个操作的复杂度为O(lg ...

  3. BZOJ.4695.最假女选手(线段树 Segment tree Beats!)

    题目链接 区间取\(\max,\ \min\)并维护区间和是普通线段树无法处理的. 对于操作二,维护区间最小值\(mn\).最小值个数\(t\).严格次小值\(se\). 当\(mn\geq x\)时 ...

  4. 【数据结构系列】线段树(Segment Tree)

    一.线段树的定义 线段树,又名区间树,是一种二叉搜索树. 那么问题来了,啥是二叉搜索树呢? 对于一棵二叉树,若满足: ①它的左子树不空,则左子树上所有结点的值均小于它的根结点的值 ②若它的右子树不空, ...

  5. 线段树(segment tree)

    线段树在一些acm题目中经常见到,这种数据结构主要应用在计算几何和地理信息系统中.下图就为一个线段树: (PS:可能你见过线段树的不同表示方式,但是都大同小异,根据自己的需要来建就行.) 1.线段树基 ...

  6. 浅谈线段树 Segment Tree

    众所周知,线段树是algo中很重要的一项! 一.简介 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 使用线段树可以快速的查找某一个节点在 ...

  7. 线段树 Interval Tree

    一.线段树 线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树. 例题:给定N条线段,{[2, 5], [4, ...

  8. 线段树(I tree)

    Codeforces Round #254 (Div. 2)E题这题说的是给了一个一段连续的区间每个区间有一种颜色然后一个彩笔从L画到R每个区间的颜色都发生了 改变然后 在L和R这部分区间里所用的颜色 ...

  9. segment树(线段树)

    线段树(segment tree)是一种Binary Search Tree或者叫做ordered binary tree.对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+ ...

  10. RMQ问题(线段树+ST算法)

    转载自:http://kmplayer.iteye.com/blog/575725 RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ ...

随机推荐

  1. python 的面相对象编程--对应c++

    在python的面相对象编程中,我们常常在class中可以看到a(),  _b() ,  __c(), __d()__这样的函数. 由于我是看廖雪峰老师的教程,廖老师为了简单起见,没有引入太多概念,我 ...

  2. 复制命令(XCOPY)

    XCOPY 命令: // 描述: 将文件或目录(包括子目录)从一个位置复制到另一个位置. // 语法: Xcopy <Source> [<Destination>] [/w] ...

  3. 【Java】代理模式、反射机制-动态代理

    关于代理模式和动态代理参考自:https://www.cnblogs.com/gonjan-blog/p/6685611.html 这里通过参考博客中的例子整理个人理解. 代理模式: 访问某个类的方法 ...

  4. android studio 将自己的项目生成jar包

    很多情况下我们开发的项目不是一个完整的app,而是完成一部分功能,供别人的app使用的情况.这时就需要将我们的项目打包生成jar或者arr库文件,让别人的app导入我们的jar包,就能直接使用我们项目 ...

  5. centos7 安装maven

    进入指定目录 cd /usr/local/src/   下载maven 包 wget http://mirrors.hust.edu.cn/apache/maven/maven-3/3.1.1/bin ...

  6. 79、iOS 的Cocoa框架、Foundation框架以及UIKit框架

    Cocoa框架是iOS应用程序的基础 1. Cocoa是什么? Cocoa是 OS X和ios 操作系统的程序的运行环境. 是什么因素使一个程序成为Cocoa程序呢?不是编程语言,因为在Cocoa开发 ...

  7. Javascript学习之:JSON

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.它是基于ECMAScript的一个子集,采用完全独立于语言的文本格式.这些特性使JSON成为理想的数据交换 ...

  8. st-link调试和下载程序(待写)

    st-link调试只用三根线 GND SWCLK SWDIO

  9. elasticsearch 安装,以及遇到的问题总结

    系统.软件环境: Centos 6.5 elasticsearch 6.1.1 elasticsearch 安装的话是很简单的,但是安装完成启动的时候报错,下面我就一一的来描述错误,并提供相应的解决方 ...

  10. 《Node.js 高级编程》简介与第二章笔记

    <Node.js 高级编程> 作者简介 Pedro Teixerra 高产,开源项目程序员 Node 社区活跃成员,Node公司的创始人之一. 10岁开始编程,Visual Basic.C ...