线段树
线段树
对于线段树中的每个结点, 其表示一个区间,我们可以记录和这个区间相关 的一些信息(如最大值、最小值、和等) ,但要满足可二分性,即能直接由其子
结点的相关信息得到。 对于询问区间信息和修改区间信息的操作,线段树一般在能 O(log n) 的时间 内完成,而且常数相对较小(和后面的伸展树比较) ,算是一种高效实用的数据 结构。 (2)线段树的基本操作——查询区间信息 使用线段树的目的就是能高效地查找和修改区间信息, 下面先介绍第一个操 作——查询操作。 对于当前要查询的区间 a, b ,我们从线段树的根结点开始,如果当前结点 表示的区间被查询区间完全包含,那么更新结果,否则分别考察左右子结点,如 果查询区间与某个子结点有交集(也可能两个都有) ,那么就递归考察这个子结 点。代码框架如下1:
对于任意一个区间,会被划分成很多在线段树上存在的区间,可以证明,划 分出来的区间在线段树的每层最多有两个,又因为线段树的深度为 O(log n) ,因 此查询操作的时间复杂度为 O(log n) 。
(3)线段树的基本操作——修改区间信息 相对于查询区间信息,修改区间信息显得稍微复杂一些。
1
本文中的代码均使用 C++语言描述
(4)线段树特点总结 利用线段树, 我们可以高效地询问和修改一个数列中某个区间的信息,并且 代码也不算特别复杂。 但是线段树也是有一定的局限性的, 其中最明显的就是数列中数的个数必须 固定,即不能添加或删除数列中的数。对于这个问题,下面介绍的伸展树就可以 完美的解决。
(7,10]
(0,1] (1,2] (2,3] (3,5] (5,6]
(6,7] (7,8]
(8,10]
(3,4]
历史区间最值线段树
历史区间最值线段树
历史区间最值线段树是一种数据结构,用于在一个固定的区间上进行最值查询,同时支持历史版本查询。
线段树是一种二叉树,其中每个节点代表一个区间,左子节点表示左半区间,右子节点表示右半区间。
每个节点保存了该区间的最值信息。
通过将整个区间递归地划分成子区间,可以构建出一棵线段树。
历史区间最值线段树基于线段树的基本思想进行扩展。
除了保存每个区间的最值信息外,它还保存了每个区间的历史版本信息。
在每次更新操作时,会复制一份当前区间的最值信息,然后在新版本中进行更新。
这样就可以记录每个区间每个版本的最值信息。
对于查询操作,可以通过二分法在历史版本上进行查找,找到指定版本的最值信息。
同时可以通过二分法在每个版本上进行查找,找到某个区间范围内的最值。
历史区间最值线段树主要用于解决一些需要查询历史版本最值信息的问题,比如求解一段时间范围内的最大或最小值。
它在某些场景下可以提供更高效的查询性能,但是也需要额外的空间来保存历史版本信息。
线段树
线段树目录定义基本结构实现代码树状数组编辑本段定义区间在[1,5]内的线段树线段树又称区间树,是一种对动态集合进行维护的二叉搜索树,该集合中的每个元素 x 都包含一个区间 Interval [ x ]。
线段树支持下列操作:Insert(t,x):将包含区间 int 的元素 x 插入到树中;Delete(t,x):从线段树 t 中删除元素 x;Search(t,i):返回一个指向树 t 中元素 x 的指针。
编辑本段基本结构线段树是建立在线段的基础上,每个结点都代表了一条线段[a , b]。
长度为1的线段称为元线段。
非元线段都有两个子结点,左结点代表的线段为[a , (a + b ) / 2],右结点代表的线段为[( a + b ) / 2 , b]。
右图就是一棵长度范围为[1 , 5]的线段树。
长度范围为[1 , L] 的一棵线段树的深度为log ( L - 1 ) + 1。
这个显然,而且存储一棵线段树的空间复杂度为O(L)。
线段树支持最基本的操作为插入和删除一条线段。
下面以插入为例,详细叙述,删除类似。
将一条线段[a , b] 插入到代表线段[l , r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。
如果b<mid,那么将线段[a , b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a , b] 也插入到p的右儿子结点中。
插入(删除)操作的时间复杂度为O (Log N)。
上面的都是些基本的线段树结构,但只有这些并不能做什么,就好比一个程序有输入没输出,根本没有任何用处。
最简单的应用就是记录线段有否被覆盖,并随时查询当前被覆盖线段的总长度。
那么此时可以在结点结构中加入一个变量int count;代表当前结点代表的子树中被覆盖的线段长度和。
这样就要在插入(删除)当中维护这个count值,于是当前的覆盖总值就是根节点的count值了。
另外也可以将count换成bool cover;支持查找一个结点或线段是否被覆盖。
线段树ppt课件
•
else Count := 0; 连接处颜色相同并且
非底色,则总数减1
统计算法
最左边的颜色
• end • else if r – l > 1 then
最右边的颜色
• begin
最左颜色=最右颜色=本身
•
result := Count(p * 2, l,非(底l +色则r)统d计iv数加2,1 lc, tl) +
•
else if a >= m then Insert(p * 2 + 1, m, r, a, b, c)
•
else begin
•
Insert(p * 2, l, m, a, m, c);
•
Insert(p * 2 + 1, m, r, m, b, c);
•
end;
•
end;
• end;
• end;
示例
• 初始情况 0 0 0 0 0
• [1,2]
10000
• [3,5]
10110
• [4,6]
10111
• [5,6]
10111
4个1
缺点
• 此方法的时间复杂度决定于下标范围的平 方。
• 当下标范围很大时([0,10000]),此方法 效率太低。
离散化的做法
• 基本思想:先把所有端点坐标从小到大排 序,将坐标值与其序号一一对应。这样便 可以将原先的坐标值转化为序号后,对其 应用前一种算法,再将最后结果转化回来 得解。
Wall
分析
• 这道题目是一个经典的模型。在这里,我 们略去某些处理的步骤,直接分析重点问 题,可以把题目抽象地描述如下:x轴上有 若干条线段,求线段覆盖的总长度。
线段树的概念与应用
线段树的概念与应用线段树(Segment Tree)是一种用于解决区间查询问题的数据结构。
它可以高效地支持以下两种操作:区间修改和区间查询。
线段树的应用非常广泛,在离线查询、区间统计、区间更新等问题中有着重要的作用。
一、概念线段树是一颗二叉树,其中每个节点代表了一个区间。
根节点表示整个待查询区间,而叶子节点表示的是单个元素。
每个内部节点包含了其子节点所代表区间的并集。
二、构建线段树线段树的构建过程是自底向上的。
将待查询数组划分成一颗满二叉树,并将每个区间的和存储在相应的节点中。
对于叶子节点,直接存储对应元素的值。
而非叶子节点的值可以通过其子节点的值计算得到。
三、线段树的查询对于区间查询操作,可以通过递归方式实现。
从根节点开始,判断查询区间和当前节点所代表的区间是否有交集。
若没有交集,则返回当前节点的默认值。
若查询区间包含当前节点所代表的区间,则返回当前节点存储的值。
否则,将查询区间分割成左右两部分继续递归查询。
四、线段树的更新对于区间更新操作,也可以通过递归方式实现。
与查询操作类似,首先判断查询区间和当前节点所代表的区间的关系。
若没有交集,则无需更新。
若查询区间包含当前节点所代表的区间,则直接更新当前节点的值。
否则,将更新操作分割成左右两部分继续递归更新。
五、应用案例:区间最值查询一个常见的线段树应用是求解某个区间的最值。
以查询区间最小值为例,可以通过线段树来高效地解决。
首先构建线段树,然后进行区间查询时,分为以下几种情况处理:若当前节点所代表的区间完全包含于查询区间,则直接返回该节点的值;若当前节点所代表的区间与查询区间没有交集,则返回默认值;否则,将查询区间分割成左右两部分继续递归查询,最后返回两个子区间查询结果的较小值。
六、总结线段树是一种非常有用的数据结构,能够高效地解决区间查询问题。
通过合理的构建和操作,线段树可以应用于多种场景,如区间最值查询、离线查询等。
熟练掌握线段树的概念和应用方法,对解决问题具有重要意义。
线段树求解区间最长递增子序列问题
线段树求解区间最长递增子序列问题区间最长递增子序列问题是指在给定的序列中找出一个区间,该区间内的子序列是递增的且具有最长长度。
为了解决这个问题,我们可以使用线段树的数据结构。
线段树是一种用于快速解决区间查询问题的树状数据结构。
对于区间最长递增子序列问题,我们可以通过建立线段树来实现。
首先,我们需要将原始序列分割成若干区间,每个区间有一个对应的节点。
每个节点存储这个区间内的最长递增子序列的长度。
然后,我们可以通过递归地建立线段树来实现。
在建立线段树时,我们首先将原始序列分为两半,然后递归地对每个子区间进行相同的分割操作。
当递归到只剩下一个元素时,该节点的最长递增子序列的长度为1。
接下来,我们需要将每个节点的子树的最长递增子序列的长度合并到父节点中。
对于一个父节点的区间来说,其最长递增子序列的长度可以由左子节点的最长递增子序列和右子节点的最长递增子序列合并得到。
具体合并的过程是比较左子节点的最长递增子序列的长度和右子节点的最长递增子序列的长度,取较大值即为父节点的最长递增子序列的长度。
当线段树建立完毕后,我们可以通过查询操作来求解区间最长递增子序列问题。
对于每个查询,我们从根节点开始,比较查询区间和当前节点的区间。
如果查询区间完全包含当前节点的区间,那么返回当前节点的最长递增子序列的长度。
如果查询区间与当前节点的区间有交集,那么递归地查找左子节点和右子节点,然后分别返回两个子节点的最长递增子序列的长度的较大值。
通过线段树,我们可以高效地解决区间最长递增子序列问题。
每次查询的时间复杂度为O(logn),其中n是序列的长度。
然而,建立线段树的时间复杂度较高,为O(nlogn)。
因此,在需要频繁查询但很少修改的情况下,使用线段树是一个高效的选择。
总结起来,线段树是一种用于解决区间查询问题的数据结构。
通过建立线段树,可以高效地求解区间最长递增子序列问题,每次查询的时间复杂度为O(logn)。
线段树板子(懒惰标记)
线段树板⼦(懒惰标记)线段树线段树就是在⼆叉树的基础上,每个节点存储⼀个区间中所有数的和。
如果⼀个节点对应的区间是 [l ,r ],那么把 [l ,r ] 分成l ,l +r 2(左⼉⼦)和 l +r 2+1,r (右⼉⼦). 根节点表⽰的区间是[1, n],这样区间 [1, n]就被划分为⼀个树形结构.需要注意的还有线段树数组的⼤⼩:线段树的深度是 ⌈logn ⌉.线段树是⼀棵完全⼆叉树,总节点个数为 2⌈logn ⌉+1−1<4n .因此我们⾄少要把线段树的节点数开成 4n .建树根据⼆叉树的性质,rt 的左⼉⼦是rt×2,右⼉⼦是rt×2+1,⽤位运算可以优化时间效率Code单点修改修改线段树上的⼀个节点最多只会影响该节点到根的路径上的这些节点,也就是最多只需要修改⌊logn ⌋个节点。
Code区间查询Code懒惰标记[⌊⌋][⌊⌋]Processing math: 100%A 的懒惰标记加 1.A 的权值加上 1 ∗ 2(加上的数字 × 区间长度).询问 [1, 2],直接返回 tree[A].A 的懒惰标记下传 (特别注意, A 的懒惰标记和 A ⽆关, A 的懒惰标记是要加在⼉⼦⾝上的,⽽不是⾃⼰⾝上) tree[B], tree[C] 加上 lazy[A] ∗ 1(懒惰标记 × 区间长度)lazy[B], lazy[C] 加上 lazy[A]直接返回 tree[C].区间修改updatapushdown类似之前的询问操作,修改操作也是将区间拆成logn个区间分别修改,时间复杂度O(logn). Code区间查询有懒惰标记相⽐没有懒惰标记的时候,区间查询唯⼀的不同就是需要下放标记. Code例题Code。
线段树 维护标记 经典题
线段树维护标记经典题
线段树是一种用于解决区间查询问题的数据结构,它可以高效地处理区间操作,例如区间最大值、区间和、区间更新等。
线段树通常用于解决动态区间查询问题,比如区间修改、区间查询等。
在线段树中,维护标记是一种常见的技巧,它可以帮助我们在区间内进行延迟更新,从而减少不必要的操作。
维护标记通常用于延迟更新操作,当我们需要对区间内的元素进行更新时,我们可以先将更新操作记录在节点上,而不立即更新区间内的所有元素,这样可以减少不必要的更新操作,提高效率。
经典题目中常涉及使用线段树维护标记的问题包括区间修改、区间查询等。
例如,区间修改问题可以是给定一个数组,进行一系列区间修改操作,然后查询某个位置的元素值;区间查询问题可以是给定一个数组,进行一系列区间查询操作,然后求解区间内的最大值、最小值、和等。
在解决经典题目时,我们需要注意线段树的构建、更新、查询等操作,以及维护标记的技巧,这样才能高效地解决问题。
同时,我们也需要注意处理边界情况和特殊情况,保证算法的正确性和鲁
棒性。
总之,线段树是一种非常有用的数据结构,通过合理地维护标记,我们可以解决许多经典的区间查询问题。
在解决这些问题时,
我们需要充分理解线段树的原理和操作,灵活运用维护标记的技巧,才能更好地解决问题。
历史版本和线段树
历史版本和线段树
历史版本和线段树是计算机科学中的两个重要概念。
历史版本,顾名思义,是指数据结构在不同时间点上的不同状态。
在软件开发中,我们经常需要对数据进行修改和更新,但有时候我们也需要回溯到前面的某个状态。
历史版本技术就是为了解决这个问题而提出的。
它可以记录数据结构在不同时间点上的状态,并且可以在需要时恢复到特定的历史状态。
这在很多应用中都非常有用,比如版本控制系统和数据库管理系统。
线段树是一种用于解决区间查询的数据结构。
它可以高效地计算一个区间内的某种属性或者进行某种操作,比如求和、最大值、最小值等。
线段树通过将区间逐步细分成更小的子区间,并且计算每个子区间的属性来构建一个树状结构,从而实现高效的查询和修改。
线段树广泛应用于各种领域,比如计算几何、图像处理、离散数学等。
历史版本和线段树都是计算机科学中非常有用的技术。
历史版本可以帮助我们记录和恢复数据结构的历史状态,而线段树可以高效地解决区间查询问题。
它们在各种应用中都发挥着重要作用,并且得到了广泛的研究和应用。
【学习笔记】权值线段树
【学习笔记】权值线段树⼀. 权值线段树权值线段树即⼀种线段树,以序列的数值为下标。
节点⾥所统计的值为节点所对应的区间 [l,r] 中,[l,r] 这个值域中所有数的出现次数。
举个例⼦,有⼀个长度为 10 的序列 {1,5,2,3,4,1,3,4,4,4}。
那么统计每个数出现的次数。
易知 1 出现了 2 次,2 出现了 1 次,3 出现了 2 次,4出现了 4次,5 出现了 1 次。
那么我们可以建出⼀棵这样的权值线段树:从⽹上搬的。
节点中的数字代表节点对应区间中所有数的出现次数。
换句话说,每个叶⼦节点的值:代表这个值的出现次数。
⾮叶⼦节点的值:代表了某⼀个值域内,所有值出现次数的和。
上⾯的权值线段树中,6,7,8 并没有出现,然⽽却被建出。
如果序列的数a i的取值范围是w,那么我们的树就需要O(w log w) 的空间。
这对于⼤部分题都是⽆法忍受的。
所以考虑动态开点。
⼀般的线段树,对于节点p,其ls,rs⼀般都是p×2,p×2+1,⽽这⾥我们直接定义两个数组ls[p],rs[p]来表⽰节点p的左右⼉⼦。
那么这样,我们会建出O(n) 个叶⼦结点,⽽对于每⼀个叶⼦结点⽹上还有O(log w) 的深度,所以总的空间复杂度降为O(n log w)。
考虑如何⽤代码实现建树的过程。
inline void pushup(int p){tr[p]=tr[ls[p]]+tr[rs[p]];return;}inline void update(int &p,int l,int r,int now){if(!p)p=++id;if(l==r){tr[p]++;return;}int mid=(l+r)>>1;if(now<=mid)update(ls[p],l,mid,now);else update(rs[p],mid+1,r,now);pushup(p);return;}我们可以实现在 update 的过程⽤中 build。
线段树入门
小结
ቤተ መጻሕፍቲ ባይዱ
其实线段树就是用某个值来代替(概括) 一段区间的值,来避免求解时用到大量的 基础元素。
框架:建树
procedure maketree(k,q,p:longint); //建树,k表示区间[q,p] 的代号 begin if q=p then begin 赋初值 exit; end; maketree(k*2,q,(q+p) div 2); //分割 访问左儿子 代号为 k*2 maketree(k*2+1,(q+p) div 2+1,p); //分割 访问右儿子 代号 为k*2+1 递归回来后由两儿子决定父亲的值 end;
线段树的定义
线段[1, 9]的线段树
[1,9] [1,4] [1,2] 1 2 [3,4] 3 4 [5,9] [5,6] 5 6 [7,9] 7 [8,9] 8 9
1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 8 9
框架:修改基础元素
procedure change(k,q,p,a,s:longint); 修改第a位置的值变为s,[q,p]为当前 查找到的区间 var m:longint; begin if (q>a) or (p<a) then //如果a不在[q,p]中,就退出 exit; if (a=q) and (q=p) then //如果查找到了a,就更新 begin 更新相关量 exit; end; m:=(q+p) div 2; insert(k*2,q,m,a,s); //分割,查找左儿子 insert(k*2+1,m+1,p,a,s); //分割,查找右儿子 用儿子的相关量更新父亲 end;
线段树详解(C++版)
build(((l+r)/2)+1,r);
t[h].data=min(t[t[h].ls].data,t[t[h].rs].data);
}
else
{
t[h].data=a[l];f[l]=h;
}
}
int find(int h,int p,int q)
{
int v;
if((t[h].l==p)&&(t[h].r==q)) return(t[h].data);
Input
输入中第一行有两个数m,n表示有m(m< =100000)笔账,n表示有n个问题,n< =100000。
接下来每行为3个数字,第一个p为数字1或数字2,第二个数为x,第三个数为y
当p=1则查询x,y区间
当p=2则改变第x个数为y
Output
输出文件中为每个问题的答案。具体查看样例。
Sample Input
if(t[x].data!=min(t[t[x].ls].data,t[t[x].rs].data))
struct ss
{
int l,r,ls,rs,f,data;
}t[400001];
void build(int l,int r)
{
int h;
num++;
h=num;
t[h].l=l;
t[h].r=r;
if(l!=r)
{
t[h].ls=num+1;
t[num+1].f=h;
l,(l+r)/2);
1、忠诚(TYVJ 1038)
Description
老管家是一个聪明能干的人。他为财主工作了整整10年,财主为了让自已账目更加清楚。要求管家每天记k次账,由于管家聪明能干,因而管家总是让财主十分满意。但是由于一些人的挑拨,财主还是对管家产生了怀疑。于是他决定用一种特别的方法来判断管家的忠诚,他把每次的账目按1,2,3…编号,然后不定时的问管家问题,问题是这样的:在a到b号账中最少的一笔是多少?为了让管家没时间作假他总是一次问多个问题。
线段树实际应用
什么是线段树线段树也被称为区间树,英文名为Segment Tree或者Interval tree,是一种高级的数据结构。
这种数据结构更多出现在竞赛中,在常见的本科数据结构教材里没有介绍这种数据结构。
但是,在面试中却有可能碰到和线段树相关的问题。
那么为什么会产生线段树这种数据结构,线段树到底是为了解决什么样的一种问题呢?其实这里的线段可以理解为区间,线段树就是为了解决区间问题的。
有一个很经典的线段树问题是:区间染色。
假设有一面墙,长度为 n,每次选择一段墙进行染色。
在区间染色的过程中,每次选择一段区间进行染色,这时新的颜色可能会覆盖之前的颜色。
最后的问题是:在经过 m 次染色操作后,我们可以在整个区间看见多少种颜色?更加普遍的说法是:在经过 m 次染色操作后,我们可以在区间 [i, j]内看见多少种颜色?由于第一个问题是第二个问题的一个特例,我们采用第二种问题来思考解决方法。
从上面可以看出,我们对于区间,有 2 种操作,分别是染色操作和查询区间的颜色,使用更加一般的说法,染色操作就是更新区间,查询区间的颜色就是查询区间。
这类问题里面,更加常见的的是区间查询:一个数组存放的不再是颜色,而是具体的数字,查询某个区间[i, j]的统计值。
这里的统计值是指:区间内最大值、最小值、或者这个区间的数字和。
比如:查询 2018 年注册的用户中消费最高的用户查询 2018 年注册的用户中消费最低的用户注意上面两种情况都是动态查询,我们查询的消费数据不只是 2018 的消费数据。
如果我们想查询 2018 年中消费最高的用户,那么 2018 年的数据已经固定了,我们直接在这一年的数据中进行统计分析就行了。
但是一个 2018 年注册的用户,在 2019 年、2020 年都可能会有消费。
我们实际上查询的是:2018 年注册的用户中,到现在为止,消费最高的用户。
这种情况下,数据是在动态变化的,也就是说:2017 年注册的用户中,每个用户的消费额是会更新的,这就对应到更新区间的操作。
线段最值问题的常用解法
线段最值问题的常用解法
线段最值问题是一个常见的数学问题,它要求在给定的一组线段中找到最大或最小值。
这类问题在计算几何、最优化和动态规划等领域中经常出现。
解决线段最值问题的常用方法包括扫描线算法、线段树和动态规划等。
扫描线算法是一种常用的解决线段最值问题的方法。
该算法的基本思想是通过将线段按照起点和终点的位置进行排序,然后从左到右扫描线段,同时维护一个当前的最值。
当扫描到一个线段时,根据线段的起点和终点更新当前最值。
这种方法的时间复杂度为O(nlogn),其
中n为线段的数量。
线段树是一种高效的数据结构,用于解决线段最值问题。
它将线段分解成一棵二叉树,并在每个节点上存储线段的最值信息。
通过构建线段树,可以快速查询任意区间的最值。
线段树的构建时间复杂度为
O(nlogn),查询时间复杂度为O(logn)。
动态规划是一种常用的解决线段最值问题的方法。
该方法通过定义状态和状态转移方程,逐步计算出最优解。
对于线段最值问题,可以将线段的起点和终点作为状态,然后根据状态转移方程更新最值。
动态规划的时间复杂度取决于状态的数量和状态转移方程的复杂度。
除了上述方法,还有一些其他的解决线段最值问题的方法,如分治法和贪心算法。
这些方法根据具体问题的特点选择合适的解决策略。
总之,线段最值问题是一个常见的数学问题,可以通过扫描线算法、线段树和动态规划等方法得到解决。
选择合适的解决方法需要根据具体问题的特点和要求进行评估和选择。
线段树进阶——权值线段树与动态开点
线段树进阶——权值线段树与动态开点前置知识:线段树权值线段树我们都知道,普通的线段树是⼀种⽤来维护序列区间最值的⼀种数据结构。
⽽权值线段树,就是将序列中各个值出现的频数作为权值,再⽤线段树来维护值域的数据结构。
与其说是⼀种数据结构,更不如说是⼀个线段树的 trick。
实际代码的话,⽤线段树维护桶数组就⾏了。
权值线段树重要作⽤是反应序列中元素的⼤⼩问题,如求第k⼤第k⼩问题。
因为本⾝就与普通线段树差不多,所以就直接放模板了((code:#include <bits/stdc++.h>using namespace std;const int N=3e5+10;int n,k_;int bucket_[N];int tree[N<<1];void push_up(int node){tree[node]=tree[node<<1]+tree[node<<1|1];}void build(int node,int start,int end){if(start==end){tree[node]=bucket_[start];return ;}int mid=start+end>>1;int lnode=node<<1;int rnode=node<<1|1;build(lnode,start,mid);build(rnode,mid+1,end);push_up(node);}void update(int node,int start,int end,int k,int val)//⼤⼩为val的数多k个,相当于单点修改{if(start==end){tree[node]+=k;return ;}int mid=start+end>>1;int lnode=node<<1;int rnode=node<<1|1;if(val<=mid) update(lnode,start,mid,k,val);else update(rnode,mid+1,end,k,val);push_up(node);}int query(int node,int start,int end,int val)//查询数字val有多少个,相当于单点查询{if(start==end) return tree[node];int mid=start+end>>1;int lnode=node<<1;int rnode=node<<1|1;if(val<=mid) return query(lnode,start,mid,val);else return query(rnode,mid+1,end,val);push_up(node);}int query_kth(int node,int start,int end,int k)//查询第k⼩{if(start==end) return start;int mid=start+end>>1;int lnode=node<<1;int rnode=node<<1|1;if(tree[lnode]>=k) return query_kth(lnode,start,mid,k);//如果左⼦树的权值⼤于k,证明第k⼩值左⼦树else return query_kth(rnode,mid+1,end,k-tree[lnode]);//进⼊右⼦树时,整个区间的第k⼩相当于右区间的第(k-左区间)⼩,记得减去左⼦树的值}Processing math: 100%if(start==end) return start;int mid=start+end>>1;int lnode=node<<1;int rnode=node<<1|1;if(tree[rnode]>=k) return query_kthbig(rnode,mid+1,end,k);//若右⼦树的权值⼤于k,证明第k⼤值在右⼦树else return query_kthbig(lnode,start,mid,k-tree[rnode]);//进⼊左⼦树时,记得减去右区间}int main(){scanf("%d%d",&n,&k_);int maxn=0,tot=0;for(int i=1; i<=n; i++){int x;scanf("%d",&x);maxn=maxn>=x?maxn:x;bucket_[x]++;}build(1,1,maxn);cout<<query_kth(1,1,maxn,k_);return 0;}板⼦题:由于重复的不算,⽤桶统计出现次数的时候只统计到1就⾏了。
前缀和和线段树
前缀和和线段树前缀和和线段树的对比如下:两者相同点:单点/区间修改,区间查询区间查询:前缀和区间修改,单点查询:差分单点修改,区间查询:树状数组,线段树区间修改,区间查询:线段树+懒标记不同点:树状数组只能维护前缀操作和(前缀和,前缀积,前缀最大最小),而线段树可以维护区间操作和。
树状数组:某些操作是存在逆元的,这样就给人一种树状数组可以维护区间信息的错觉:维护区间和,模质数意义下的区间乘积,区间xor 和。
能这样做的本质是取右端点的前缀和,然后对左端点左边的前缀和的逆元做一次操作,所以树状数组的区间询问其实是在两次前缀和询问。
所以我们能看到树状数组能维护一些操作的区间信息但维护不了另一些的:最大/最小值,模非质数意义下的乘法,原因在于这些操作不存在逆元,所以就没法用两个前缀和做区间查询。
线段树线段树就不一样了,线段树直接维护的就是区间信息,所以一切满足结合律的操作都能维护区间和,并且lazy标记的存在还能使线段树能够支持区间修改,这点是树状数组做不到的。
可以说树状数组能做的事情其实是线段树的一个子集,大多数情况下使用树状数组真的只是因为它好写并且常数小而已。
不过随着线段树的普及,树状数组仅有的两点优势也不复存在了……估计要成为时泪了吧。
学过前缀和、差分、树状数组,但是,没有系统性学习过,今天遇到个差分的题给整神了,用树状数组能解但是写不出来(无能狂怒)。
于是系统性学习来了总结一下:数组不变,区间查询:前缀和、树状数组、线段树。
数组单点修改,区间查询:树状数组、线段树。
数组区间修改,单点查询:差分、线段树。
数组区间修改,区间查询:线段树。
区间数量统计 算法
区间数量统计算法区间数量统计是一种常见的统计问题,即统计给定区间内符合特定条件的元素数量。
该问题在很多应用场景中都有实际需求,如在计算机科学中的数据处理和图像分析中,以及在金融学和统计学中的风险分析和概率分布等领域。
在本文中,我们将介绍几种常见的区间数量统计算法,包括朴素算法、排序算法和线段树算法,并分析它们的时间复杂度和空间复杂度。
一、朴素算法朴素算法是最简单直接的算法,它通过遍历整个数据集来统计符合条件的元素数量。
具体步骤如下:1. 定义统计变量count,初始化为0。
2. 遍历数据集中的每个元素,判断元素是否在给定区间内,并将符合条件的元素数量加1。
3. 返回count作为结果。
朴素算法的时间复杂度为O(n),其中n表示数据集的大小。
该算法的优点是简单易实现,适用于小规模的数据集。
但当数据规模较大时,朴素算法的效率较低。
二、排序算法排序算法是一种常见的区间数量统计算法。
它通过对数据集进行排序,并使用一些优化方法来加速区间数量统计的过程。
具体步骤如下:1. 将数据集按照元素的大小进行排序。
2. 对给定区间的起点和终点进行二分查找,找到它们在排序后数组中的位置。
3. 统计位于起点和终点之间的元素数量,并返回结果。
排序算法的时间复杂度主要取决于排序过程,通常为O(nlogn),其中n表示数据集的大小。
排序算法的优点是能够处理大规模的数据集,并且可以进行一些优化,如快速排序、归并排序等。
三、线段树算法线段树算法是一种高效的区间数量统计算法,它通过建立一棵二叉树来表示给定区间内元素的数量。
具体步骤如下:1. 定义线段树的结构,包括节点的起点、终点和数量信息。
2. 将数据集表示为一个线段树,其中每个叶子节点表示一个元素,内部节点表示其子节点中元素的数量。
3. 根据给定区间的起点和终点,从根节点开始向下遍历线段树,统计位于起点和终点之间的元素数量,并返回结果。
线段树算法的时间复杂度为O(logn),其中n表示数据集的大小。
Segment Tree 线段树
---梁国秋
主讲内容
线段树慨况
线段树的定义 线段树的建立
线段树应用
HDU 3074 HDU 1754 POJ 1177 POJ 3667 POJ 2104
其他
认识归并树 认识划分树 建议练习
引例
• 假设有一列数{Ai}(1≤i≤n),支持如下两种操作:
– 将Ak的值加D。(k, D是输入的数) – 输出As+As+1+…+At。(s, t都是输入的数,S≤T)
例一:POJ 1177 picture
墙上贴了一些矩形的张贴画和照片。他们的边都是垂直 或者水平的。每个矩形可以部分后者全部被其他的矩形覆盖。 把这些矩形看作一个整体,他们的边界长度就是他们的轮廓 长度。所有顶点坐标都是整数。 任务:写一个程序计算轮廓长度。下面的图1是一个7个矩 形的例子:
图1:七个矩形 这些矩形的轮廓如图2所示
引例
• 数列长度 (1<=10000<=n) • 假设询问次数是M • (1<=M<=10000) • 如果用模拟算法 • Add 每次O(1) • Sum 每次O(n) • 时间复杂度为O(n*m) • 有什么好的办法吗?
线段树的定义
表示区间[1, 10]的线段树:
[1,10] [1,5] [1,3] [1,2] [2,3] [3,5] [3,4] [4,5] [5,6] [5,7] [6,7] [5,10] [7,10] [7,8] [8,10]
•
•
•
•
详细代码
想一想:
• 但是如果同样的题目,M再扩大10倍,或者100倍 呢???
HDU 3074 Multiply game
线段树_刘汝佳(有版权)
SUM的计算
• 右图表示影响 SUM(7, 9)的所 有区间
– 影响全部: [1,9], [5,9], [7,9] – 影响部分: 7, [8,9], 8, 9
[1,2] 1 2 [1,9] [1,4] [3,4] 3 4 [5,9] [5,6] 5 6 [7,9] 7 [8,9] 8 9
完整的算法
– 得到讨论区间(可能要先离散化) – 设计区间附加信息和维护/统计算法
• 线段树自身没有任何数据, 不像BST一样有 一个序关系 • 警告 想清楚 警告: 想清楚附加信息的准确含义 不能有 准确含义, 准确含义 半点含糊! • 建议 先设计便于解题 建议: 便于解题的附加信息,如果难 便于解题 以维护就加以修改
[3,4]
5 3 3 5 5 1 1
[4,5]
5 3 3 5
1 2 2 3 3 4 4 5
1 2 2 3 3 4 4 5
1 2 2 3 3 4 4
1 2 2 3 3 4 4 5
矩形树
• 每个矩形分成四份
– 空间复杂度:XY – 时间复杂度:X+Y
(x2,y2) (1,1)
(4,3)
Son1
Son2
– ADD: 给i对应结点的所有直系祖先s值增加k – SUM: 做区间分解, 把对应结点的s值相加
动态统计问题II
• 包含n个元素的数组A
– ADD(i, j, k): 给A[i], A[i+1], … A[j]均增加k – QUERY(i): 求A[i]
• 先看看是否可以沿用刚才的附加信息
– QUERY(i)就是读取i对应的结点上的s值 – ADD呢? 极端情况下, 如果是修改整个区间, 则 所有结点都需要修改!
线段树及其应用场景
线段树是一种二叉树结构,用于解决区间问题,如求区间和、最大值、最小值等。
线段树的核心思想是将每个节点预先维护好所对应区
间所需要的信息。
对于一次查询,将询问区间拆分到线段树对应的节点上,通过合并这些节点已经处理好的信息快速得到答案。
对于一次单点修改,将其对应的叶子节点到根的所有节点信息更新。
在线段树的每个节点加入一个标记,代表该节点对应的区间是否修改。
对于一次区间操作,仿照之前询问的方法,将要修改的区间拆分到线段树的节点上,对相应节点进行整段的修改,同时更新该节点的懒标记。
线段树能把一些对于区间(或者线段)的修改、维护,从O(N)的时间复杂度变成O(logN)。
它在处理一些特定的区间问题时,效率比常规方法要高很多。
总之,线段树是一种非常实用的数据结构,在解决一些特定的区间问题时具有高效且实用的优点。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
线段树节省时间原因 穿插页面
• 预处理大量数据 • 以少量空间换取大量时间 • 减少重复运算 • 基于点的分治效率高于基于边的分治
穿插页面
黑白树
• 你拥有一棵有N 个结点白色的树——所有节点都 是白色的。 • 接下来,你需要处理C 条指令: • 1.修改指令:改变一个给定结点的颜色(白变黑 ,黑变白); • 2.查询指令:询问从结点1 到一个给定结点的路 径上第一个 • 黑色结点编号。 • 数据范围: • N 《=1000000,C《= 1000000
穿插页面
目标
1 将题目用线段树的方法分析 构造线段树(算法简单,易用) 2 线段树基本操作 3 线段树与其他方法的比较
穿插页面
题型
• 区间(一维) (盒子问题3问) 二叉树 • 图形的面积、周长等有关的问题 (线段树也能扩展到二维形式,一种是“树套树” ,即先处理一维,再处理第二维;另一种是“四 叉树”。四叉树不能保证O(log^2 n)的单次操作时 间;而前者看似结构麻烦,实际上往往只需把前 面介绍的数组式实现扩展到二维(使用一个二维 数组)即可。)
穿插页面
• Step 2: • 如果查询结点3与结点6的公共祖先,则 考虑在访问顺序中 • 3第一次出现,到6第一次出现的子序 列: 3 2 4 5 4 6. • 这显然是由结点3到结点6的一条路径. • 在这条路径中,深度最小的就是最近 公共祖先(LCA). 即 • 结点2是3和6的LCA.
穿插页面
二分递归求解
穿插页面
RMQ建树
• procedure settree(xa,xb:integer); var now:integer; begin inc(tot);now:=tot; tree[now].a:=xa;tree[now].b:=xb; if xa=xb then tree[now].s:=a[xa] else begin tree[now].lch:=tot+1;settree(xa,(xa+xb) div 2); tree[now].rch:=tot+1;settree((xa+xb)div 2+1,xb); if tree[tree[now].lch].s<tree[tree[now].rch].s then tree[now].s:=tree[tree[now].lch].s else tree[now].s:=tree[tree[now].rch].s; end; end;
穿插页面
• 转化后得到的数列长度为树的结点数的两 倍加一,所以转化后的RMQ问题与LCA问 题的规模同次。
穿插页面
• 再举一个例子帮助理解: • 1 2 7 3 4 8 5 6
穿插页面
• 一个nlogn 预处理,O(1)查询的算法. • Step 1: • 按先序遍历整棵树,记下两个信息:结 点访问顺序和结点深度. • 如上图: • 结点访问顺序是: 1 2 3 2 4 5 4 6 4 2 1 7 8 7 1 //共2n-1个值 • 结点对应深度是: 0 1 2 1 2 3 2 3 2 1 0 1210
穿插页面
统计算法
function Count(p: Integer): Integer; 被完全覆盖 begin if Tree[p].cover = 1 then 是单位区间 Count := Tree[p].e – Tree[p].b else if Tree[p].e – Tree[p].b = 1 then Count := 0 else Count := Count(p * 2) + Count(p * 2 + 1); end;
• Step 3: • 于是问题转化为, 给定一个数组R,及两 个数字i,j,如何找出 • 数组R中从i位置到j位置的最小值.. • 如上例,就是 R[]={0,1,2,1,2,3,2,3,2,1,0,1,2,1,0}. • i=2;j=7; • 接下来就是经典的RMQ问题.
穿插页面
总结: 总结
• RMQ是给定一列数,动态询问[i,j]区间内的最 小(或最大值)。 LCA是给定一棵树,动态询问u和v的最近公共祖 先。 • RMQ和LCA可以相互转化。。 所以只要记住一 种就行了。。 • RMQ转LCA的时候是生成一棵类似于堆的递归树 ;LCA转RMQ的时候用到的是深度优先遍历。
穿插页面
构造
•
在此我们可以举一个例子来说明线段树 通常的构造方法
RMQ (Range Minimum Query)问题 穿插页面
• 对于长度为n的数列A,回答若干询问 RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j] 里的最小(大)值下标。 • 这时一个RMQ问题的例子: • 对数列:5,8,1,3,6,4,9,5,7 有: RMQ(2,4)=3 RMQ(6,9)=6
穿插页面
初级算法
• 每个RMQ 都独立分析 • 类似于插入排序 • 时间复杂度也类似
•
穿插页面 构造的时候,让根节点表示区间[1,N],
即所有N个数所组成的一个区间,然后,把 区间分成两半,分别由左右子树表示。由 完全二叉树性质,这样的线段树的节点数 只有2N-1个,是O(N)级别的,如图: • log ( L - 1 ) + 1
完全二叉树静态存储 穿插页面
• type • TreeNode = record • b, e: Integer; • cover: Integer; • end; • var
线段树 对应区间
tree:array[1..n] of treenode;
线段树的基本操作: 线段树的基本操作: 穿插页面
LCA(T,u,v)= E[RMQ(D,R[u],R [v])] 穿插页面
• • • • • 数列E[i]为:1,2,1,3,4,3,5,3,1 R[i]为:1,2,4,5,7 D[i]为:0,1,0,1,2,1,2,1,0 于是有: LCA(T,5,2) = E[RMQ(D,R[2],R[5])] = E[RMQ(D,2,7)] = E[3] = 1 • LCA(T,3,4) = E[RMQ(D,R[3],R[4])] = E[RMQ(D,4,5)] = E[4] = 3 • LCA(T,4,5) = E[RMQ(D,R[4],R[5])] = E[RMQ(D,5,7)] = E[6] = 3
穿插页面
比较
• ST(SparseTable 是一种高效打表)算法 动态规划思想 空间 NLOGN ST思考费时,都便于理解,结果相近 线段树应熟练掌握
来源POJ 3264
穿插页面
转化
• 有些问题看似并非线段树(并不直接涉及 区间等概念) 但分析后可转化为线段树
• 二、最近公共祖先(Least Common Ancestors) 最近公共祖先 对于有根树T(二叉树)的两个结点u、v,最近 穿插页面 公共祖先LCA(T,u,v)表示一个结点x,满足x是u、 v的祖先且x的深度尽可能大。另一种理解方式是 把T理解为一个无向无环图,而LCA(T,u,v)即u到v 的最短路上深度最小的点。 这里给出一个LCA的例子: 例一 对于T=<V,E> T=<V,E> V={1,2,3,4,5} E={(1,2),(1,3),(3,4),(3,5)} 则有: LCA(T,5,2)=1 LCA(T,3,4)=3 LCA(T,4,5)=3
穿插页面
• 对于每个节点,不但要知道它所表示的区 间,以及它的儿子节点的情况,也记录一 些别的值,不然,一棵孤零零的树能有什 么用?在这个例子里,由于要查询的东西 是最小值,不妨在每个节点内记录下它所 表示区间中的最小值。这样,根据一个线 性表构造出线段树的方法也就简单明白了 :
线段树的存储结构 穿插页面
•
穿插页面
定义
•
记为T(A,B), 线段树是一棵完全二叉树 ,记为 参数a,b表示区间 表示区间[a,b]。其中 的长度称 参数 表示区间 。其中b-a的长度称 为区间长度,记为L。 为区间长度,记为 。若L=1则表示叶子结 则表示叶子结 点。 • 若L>1,每一个叶子节点表示了一个单位 , 区间。 区间。对于每一个非叶结点所表示的结点 [a,b],其左儿子表示的区间为 ,其左儿子表示的区间为[a,(a+b)/2], , 右儿子表示的区间为[(a+b)/2,b]。 而 右儿子表示的区间为 。
穿插页面
为叙述的方便,这里以完全二叉树为例 ,说明线段树的基本操作。
穿插页面
var m: integer; begin
插入算法Insert
procedure insert(p, a, b: integer);
if tree[p].cover = 0 then{此间未被完全覆盖} {此间未被完全覆盖} begin m := (tree[p].b + tree[p].e) div 2;{取中值} {取中值} if (a = tree[p].b) and (b = tree[p].e) then {完覆盖} 完覆盖} tree[p].cover := 1 else if b <= m then insert(p * 2, a, b){在左边} {在左边} else if a >= m then insert(p * 2 + 1, a, b){在右边} {在右边} else begin insert(p * 2, a, m); {分} insert(p * 2 + 1, m, b); end;
• RMQ问题与LCA问题的关系紧密,可以相 穿插页面 互转换,相应的求解算法也有异曲同工之 妙。下面给出LCA问题向RMQ问题的转化 方法。
穿插页面
• 对树进行深度优先遍历,每当“进入”或 回溯到某个结点时,将这个结点的深度存 入数组E最后一位。同时记录结点i在数组中 第一次出现的位置(事实上就是进入结点i时 记录的位置),记做R[i]。如果结点E[i]的深 度记做D[i],易见,这时求LCA(T,u,v),就 等价于求E[RMQ(D,R[u],R [v])], (R[u]<R[v])。例如,对于第一节的例一,求 解步骤如下: