线段树(segment tree)
线段树是一种二叉搜索树,它的每一个结点对应着一个区间[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)的更多相关文章
- 『线段树 Segment Tree』
更新了基础部分 更新了\(lazytag\)标记的讲解 线段树 Segment Tree 今天来讲一下经典的线段树. 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间 ...
- 线段树(Segment Tree)(转)
原文链接:线段树(Segment Tree) 1.概述 线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,基本能保证每个操作的复杂度为O(lg ...
- BZOJ.4695.最假女选手(线段树 Segment tree Beats!)
题目链接 区间取\(\max,\ \min\)并维护区间和是普通线段树无法处理的. 对于操作二,维护区间最小值\(mn\).最小值个数\(t\).严格次小值\(se\). 当\(mn\geq x\)时 ...
- 【数据结构系列】线段树(Segment Tree)
一.线段树的定义 线段树,又名区间树,是一种二叉搜索树. 那么问题来了,啥是二叉搜索树呢? 对于一棵二叉树,若满足: ①它的左子树不空,则左子树上所有结点的值均小于它的根结点的值 ②若它的右子树不空, ...
- 线段树(segment tree)
线段树在一些acm题目中经常见到,这种数据结构主要应用在计算几何和地理信息系统中.下图就为一个线段树: (PS:可能你见过线段树的不同表示方式,但是都大同小异,根据自己的需要来建就行.) 1.线段树基 ...
- 浅谈线段树 Segment Tree
众所周知,线段树是algo中很重要的一项! 一.简介 线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点. 使用线段树可以快速的查找某一个节点在 ...
- 线段树 Interval Tree
一.线段树 线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树. 例题:给定N条线段,{[2, 5], [4, ...
- 线段树(I tree)
Codeforces Round #254 (Div. 2)E题这题说的是给了一个一段连续的区间每个区间有一种颜色然后一个彩笔从L画到R每个区间的颜色都发生了 改变然后 在L和R这部分区间里所用的颜色 ...
- segment树(线段树)
线段树(segment tree)是一种Binary Search Tree或者叫做ordered binary tree.对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+ ...
- RMQ问题(线段树+ST算法)
转载自:http://kmplayer.iteye.com/blog/575725 RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ ...
随机推荐
- 在IDEA中配置spring boot项目的热更新
在我使用IDEA的过程中,我发现Spring Boot项目本来自带的一个热部署工具无法使用,这里在参考各家博客后给出解决方案: 修改POM 首先POM文件需要包含spring boot的热部署工具,m ...
- Spring AOP中pointcut expression表达式
Pointcut 是指那些方法需要被执行"AOP",是由"Pointcut Expression"来描述的. Pointcut可以有下列方式来定义或者通过&am ...
- java基础-三元运算符
1.三元运算符的格式 /* 三元运算符 (条件表达式)?表达式1:表达式2; 如果条件为true,整个表达式结果是表达式1: 如果条件为false,整个表达式结果是表达式2: 注意:三元运算符不能单独 ...
- jenkins net编译部署 笔记 tips
1 忘记密码 的话,C:\Users\quyongshuo.jenkins\config.xml 修改 true 为false 重新启动 可以重新设置用户信息. 2 修改端口 Java -jar je ...
- 11.翻译系列:在EF 6中配置一对零或者一对一的关系【EF 6 Code-First系列】
原文链接:https://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-fi ...
- 机器翻译质量评测算法-BLEU
机器翻译领域常使用BLEU对翻译质量进行测试评测.我们可以先看wiki上对BLEU的定义. BLEU (Bilingual Evaluation Understudy) is an algorithm ...
- ZZNU 2182 矩阵dp (矩阵快速幂+递推式 || 杜教BM)
题目链接:http://47.93.249.116/problem.php?id=2182 题目描述 河神喜欢吃零食,有三种最喜欢的零食,鱼干,猪肉脯,巧克力.他每小时会选择一种吃一包. 不幸的是,医 ...
- Javascript百学不厌 - this
最近看了一本书,让自己的野路子走走正规路线 方法调用模式: 方法:当一个函数被保存为对象的一个属性时,我们称它为一个方法. var obj = { fun1: function() {this} // ...
- struts2框架学习笔记1:搭建测试
Servlet是线程不安全的,Struts1是基于Servlet的框架 而Struts2是基于Filter的框架,解决了线程安全问题 因此Struts1和Struts2基本没有关系,只是创造者取名问题 ...
- Angular使用总结 --- 如何正确的操作DOM
无奈接手了一个旧项目,上一个老哥在Angular项目中大量使用了JQuery来操作DOM,真的是太不讲究了.那么如何优雅的使用Angular的方式来操作DOM呢? 获取元素 1.ElementRef ...