线段树入门 (zz)
线段树入门(二)
线段树入门(二)动态地处理问题,比如说一些需要用到删除和修改的统计。
问题:在数轴上进行一系列操作。
每次操作有两种类型,一种是在线段[a,b]上涂上颜色,另一种是将[a,b]上的颜色擦去。
经过一系列的操作后,有多少条单位线段[k,k+1]被涂上了颜色。
分析:线段的删除。
我们原先讲的线段的删除只能是把已经添加的线段删除掉。
如我们没有添加[3,6]这条线段,删除[3,6]就是无法进行的。
例如本题中,我们先给[1,15]涂色,再擦去[4,9],就无法执行。
下面我们对原先的线段树进行改进。
给线段树的每一个结点增加一个标记域bj。
先看一个例子:如线段树的以[1,5]为根的线段树的全部结点都被涂色。
现把[1,5]上的颜色擦去,可能需要对整个[1,5]的结点(包括子结点)进行删除。
但有时没有必要,如删除[1,5]之后,只询问[3,5]的状态,则对[1,2]、[2,3]、[1,3]、[3,4]、[4,5]的修改都没有必要。
引入标记域后,可以省去这些操作。
1)擦去线段[a,b]后,给它的左子结点和右子结点加上标记,令它们的bj=-1。
2)每访问一条线段,首先检查它是否被标记,若其bj=-1,则进行如下操作:①将该线段的状态设为未被覆盖,并把该线段设为未被标记,bj=0②将该线段的左右子结点均设为被标记,bj=-1。
这样就不需要对整个线段树进行修改了。
以线段[3,4]为例。
若以后有必要访问[3,4],则必然先访问到它的父结点[3,5],而[3,5]的bj=-1,因此进行①、②的操作后,[3,5]的状态变为未被覆盖,并且把他的标记传递给了他的子结点——[3,4]和[4,5]。
接着访问[3,4]的时候,它的bj=-1,我们又把[3,4]的状态变为未被覆盖。
可见,标记会顺着访问[3,4]的路一直传递到[3,4],使得我们知道要对[3,4]的状态进行修改,避免了错误的产生。
同时,当我们需要用到[3,4]的时候才会进行修改,如果根本不需要用它,修不修改都无所谓了,并不会影响程序的正确性。
线段树讲解
关于线段树数据结构的研究线段树(Segment Tree)是一种高级的数据结构,顾名思义,它既是线段也是树,并且是一棵二叉树。
线段树的每个节点是一条线段[a,b],每条线段的左右儿子线段分别是该线段的左半区间[a,(a+b)/2]和右半区间[(a+b)/2+1,b],递归定义之后就是一棵线段树。
因此,线段树是平衡二叉树,且最后的叶子节点数为N,即整个线段区间的长度。
要知道,在不同的题目中,线段可以有不同的含义,如数轴上的一条真实的线段,或者是一个序列的连续子序列。
使用线段树可以快速地查找某一节点在若干条线段中出现的次数,时间复杂度是O(logN)。
图示如下:[1,8]/ \[1,4] [5,8]/ \ / \[1,2] [3,4] [5,6] [7,8]/ \ / \ / \ / \[1,1] [2,2] [3,3 ] [4,4] [5,5][6,6] [7,7] [8,8]线段树有以下操作:⑴区间查询询问某段区间的某些性质(极值,求和)⑵区间更新某些操作影响了某段区间(统一加了一个值)⑶三个问题①更新点,查询区间②更新区间,查询点③更新区间,查询区间定义线段树的数据结构Struct tree{Int left, right; //区间的端点Int max, sum; //视题目要求而定,重点维护的信息}tree[N*4]; //线段树空间应为原数组长度的4倍我们用一个一维的结构体数组tree[N*4]来记录节点,且根节点的下标为1,则对于任意非叶子节点tree[k],它的左二子为tree[2*k],它的右儿子为tree[2*k+1].线段树的代码实现(建树)V oid build (int id, int l, int r){Tree[id],left=l;Tree[id].right=r;If(l==r)Tree[id].sum=l;Tree[id].max=l;Else{Int mid=(l+r)/2;Build(id*2, l, r);Build(id*2+1, l, r);Tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;Tree[id].max=max(tree[id*2].max,tree[id*2+1].max);}} // 如果原数组从a[1]~a[n],调用build(1, 1, n)即可线段树的点更新V oid update (int id, int pos, int val ){If(tree[id].left==tree[id].right)Tree[id].sum=val;Else{Int mid=(tree[id].left+tree[id].right)/2;If(pos<=mid)Update(id*2, pos, val);ElseUpdate(id*2+1, pos, val);Tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;}}线段树的查询int query (int id, int l, int r){if(tree[id].left==l&&tree[id].right==r)return tree[id].sum;else{int mid=(tree[id].left+tree[id].right)/2;if(r<=mid)return query(id*2, l, r); // 待查区间在其左子区间中else if(l>mid)return query(id*2+1, l, r); // 待查区间在其右子区间中else return query(id*2, l, mid)+query(id*2+1, mid+1, r);//待查区间横跨其左右子区间}}很多时候,将问题建模成数轴上的问题或是数列上的问题后,具体地操作一般是每次对数轴上的一个区间或是数列中的连续若干个数进行一种相同的处理,比如统一加上一个数,如果处理只针对各个元素逐个进行,导致算法的效率较低。
线段数讲义
线段树入门(一)路桥中学陈朝晖今天,我们来介绍一种非常特殊的数据结构——线段树。
首先,来看这个问题:给你n个数,仅有两种操作:(1)给第i个数的值添加x(2)询问区间[a,b]的总和是多少CODEVS 1080 线段树练习时间限制: 1s 空间限制: 128000 KB 题目等级 : 钻石 Diamond题目描述 Description一行N个方格,开始每个格子里都有一个整数。
现在动态地提出一些问题和修改:提问的形式是求某一个特定的子区间[a,b]中所有元素的和;修改的规则是指定某一个格子x,加上或者减去一个特定的值A。
现在要求你能对每个提问作出正确的回答。
1≤N<100000,,提问和修改的总数m<10000条。
输入描述 Input Description输入文件第一行为一个整数N,接下来是1行共n个整数,表示格子中原来的整数。
下面是一个正整数m,接下来有m行,表示m个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和。
输出描述 Output Description共m行,每个整数样例输入 Sample Input645621341 3 52 1 41 1 92 2 6样例输出 Sample Output2222数据范围及提示 Data Size & Hint1≤N≤100000, m≤10000 。
从题目中所给的数据来看,数据量非常大,所需要的查询次数同样非常多。
#include <iostream>usingnamespace std;int dat[Maxn],N,M;struct Tree{int left,right;longlong sum;}tr[Maxn<<2];//tr[i]表示第i线段树,其中[left,right]表示数据区域的边界,sum表示线段树中当前这一段的总和//数组中的每一个结点都将最终设置在线段树的叶子节点位置上,//而线段树中还存在内部结点的存储。
线段树从入门到进阶
线段树从⼊门到进阶线段树是什么??线段树怎么写??如果你在考提⾼组前⼀天还在问这个问题,那么你会与⼀等奖失之交臂;如果你还在冲击普及组⼀等奖,那么这篇博客会浪费你⼈⽣中宝贵的5~20分钟。
上⾯两句话显⽽易见,线段树这个数据结构是⼀个从萌新到正式OI选⼿的过渡,是⼀个⾮常重要的算法,也是⼀个对于萌新来说较难的算法。
不得不说,我学习了这个算法5遍左右才有勇⽓写的这篇博客。
但是,对于OI正式选⼿来说,线段树不是算法,应该是⼀种⼯具。
她能把⼀些对于区间(或者线段)的修改、维护,从O(N)的时间复杂度变成O(logN)。
废话不说,这篇博客会分为四部:第⼀部:线段树概念引⼊第⼆部:简单(⽆pushdown)的线段树第三部:区间+/-修改与查询第四部:区间乘除修改与查询总结第⼀部 概念引⼊线段树是⼀种⼆叉树,也就是对于⼀个线段,我们会⽤⼀个⼆叉树来表⽰。
⽐如说⼀个长度为4的线段,我们可以表⽰成这样:这是什么意思呢? 如果你要表⽰线段的和,那么最上⾯的根节点的权值表⽰的是这个线段1~4的和。
根的两个⼉⼦分别表⽰这个线段中1~2的和,与3~4的和。
以此类推。
然后我们还可以的到⼀个性质:节点i的权值=她的左⼉⼦权值+她的右⼉⼦权值。
因为1~4的和就是等于1~2的和+2~3的和。
根据这个思路,我们就可以建树了,我们设⼀个结构体tree,tree[i].l和tree[i].r分别表⽰这个点代表的线段的左右下标,tree[i].sum表⽰这个节点表⽰的线段和。
我们知道,⼀颗⼆叉树,她的左⼉⼦和右⼉⼦编号分别是她*2和她*2+1再根据刚才的性质,得到式⼦:tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;就可以建⼀颗线段树了!代码如下:inline void build(int i,int l,int r){//递归建树tree[i].l=l;tree[i].r=r;if(l==r){//如果这个节点是叶⼦节点tree[i].sum=input[l];return ;}int mid=(l+r)>>1;build(i*2,l,mid);//分别构造左⼦树和右⼦树build(i*2+1,mid+1,r);tree[i].sum=tree[i*2].sum+tree[i*2+1].sum;//刚才我们发现的性质return ;}嗯,这就是线段树的构建,你可能会问为什么要开好⼏倍的内存去储存⼀条线段。
线段树
对于线段树中的每个结点, 其表示一个区间,我们可以记录和这个区间相关 的一些信息(如最大值、最小值、和等) ,但要满足可二分性,即能直接由其子
结点的相关信息得到。 对于询问区间信息和修改区间信息的操作,线段树一般在能 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]
线段树入门
线段树入门一、引言我们在做练习和比赛中,经常能碰见统计类型的题目。
题目通过输入数据给程序提供事物信息,并要求程序能比较高效地求出某些时刻,某种情况下,事物的状态是怎样的。
这类问题往往比较简单明了,也能十分容易地写出模拟程序。
但较大的数据规模使得模拟往往不能满足要求。
于是我们就要寻找更好的方法。
本文将介绍解决此类问题的一种方法——线段树。
二、线段树2.1 线段树的结构线段树是一棵二叉树,其结点是一条“线段”——[a,b],它的左儿子和右儿子分别是这条线段的左半段和右半段,即[a,[(a+b)/2]],[[(a+b)/2],b]线段树的叶子结点是长度为1的单位线段[a,a+1]。
下图就是一棵根为[1,10]的线段树:易证一棵以[a,b]为根的线段树结点数是2*(b-a)-1。
由于线段树是一棵平衡树,因此一棵以[a,b]为根结点的线段树的深度为log2(2*(b-a))。
线段树中的结点一般采取如下数据结构:其中a,b分别表示线段的左端点和右端点,Left,Right表示左儿子和右儿子的编号。
因此我们可以用一个一维数组来表示一棵线段树:Tree:array[1..Maxn] of TreeNode;a,b,Left,Right这4个域是描述一棵线段树所必须的4个量。
根据实际需要,我们可以增加其它的域,例如增加Cover域来计算该线段被覆盖的次数,bj域用来表示结点的修改标记(后面将会提到)等等。
2.2 线段树的建立我们可以用一个简单的过程建立一棵线段树。
Procedure MakeTree(a,b)Var Now:Longint;BeginInc(tot);Now:=tot;Tree[now].a:=a;Tree[now].b:=b;If a+1<b thenBeginTree[now].left:=tot+1;Maketree(a,(a+b)shr 1);Tree[now].right:=tot+1;Maketree((a+b)shr 1,b);End;End2.3 线段树中的线段插入和删除增加一个Cover的域来计算一条线段被覆盖的次数,即数据结构变为:因此在MakeTree的时候应顺便把Cover置0。
线段树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)首先,将整个区间 $[1,n]$ 分为两个部分,设左边区间为$[1,mid]$,右边区间为 $[mid+1,n]$。
这里的 $mid$ 是 $(1+n)/2$ 的值。
(2)然后,将左区间和右区间再分别划分成两个子区间并进行相加,直到区间大小为 1,构建出一棵完整的线段树。
三、线段树的维护构建好线段树之后,我们需要对其进行维护,以便能够快速地查询给定区间的值。
设线段树中某个节点代表区间 $[l,r]$,那么这个节点的值等于 $[l,r]$ 区间中所有元素的和。
如果需要对线段树进行更新,我们可以利用递归的方式向下更新每个节点的值。
当需要将 $[l,r]$ 区间中的某个元素修改为 $x$ 时,我们可以将其视为将线段树上区间 $[l,r]$ 的值都减去原来元素的值再加上 $x$。
四、线段树的查询线段树的查询包括单点查询和区间查询两种方式:(1)单点查询:即查询线段树中某个节点所代表的区间的值。
(2)区间查询:即查询线段树中 $[l,r]$ 区间内所有元素的和。
五、应用实例下面通过几道例题来说明线段树的应用。
问题一:给定一个序列,更新其中某个元素的值,并求出其区间和。
样例输入:8 1 3 -4 2 8 10 9 62 5 2样例输出:17问题二:对一个序列进行 $m$ 次操作,每次操作为在 $L$ 到 $R$ 的区间内加上 $c$,并输出最终改变后的序列。
样例输入:5 31 3 23 5 32 4 1样例输出:2 5 4 0 2以上就是关于线段树的详细介绍和几个应用示例,希望可以对读者有所帮助。
统计的力量——线段树详细教程
清华大学 张昆玮
24
2019年9月7日
*我们可以直接找到一个数对应的叶子 *不用自顶向下慢慢地找啊找 *“直接加 4 ”多简单!
*…… *直接找到叶子? *无限遐想中……
*
清华大学 张昆玮
25
2019年9月7日
*
清华大学 张昆玮
可以直接找到叶子?
26
2019年9月7日
(0,5)?
1
2
3
4
5
6
*没了? *没了。
*
清华大学 张昆玮
34Βιβλιοθήκη 2019年9月7日*仅使用了两倍原数组的空间 *其中还完整地包括了原数组 *构造容易:
* For i=M-1 downto 1 do T[i]=T[2i]+T[2i+1];
*太好写了!好理解! *自底向上,只访问一次,而且不一定访问到顶层 *实践中非常快,与树状数组接近 *为什么呢?后面再讲。
2019年9月7日
*我之前用这种写法做过不少题…… *大家都说我的代码看不懂 *我说这就是一个树状数组 *写树状数组的人说没有lowbit *我说那就算是线段树吧 *大家不相信非递归的线段树这么短…… *我:……
*
清华大学 张昆玮
43
2019年9月7日
*
清华大学 张昆玮
44
懒惰即美德。
2019年9月7日
13
2019年9月7日
*原因是区间和的性质非常好 *满足区间减法 *区间减法什么的最讨厌了!后面再说! *不过直接前缀和不是万能的! *如果修改元素的话……
清华大学 张昆玮
*
14
2019年9月7日
数据结构 直接存储原数组 直接存储前缀和
线段树的概念与应用
线段树的概念与应用线段树(Segment Tree)是一种用于解决区间查询问题的数据结构。
它可以高效地支持以下两种操作:区间修改和区间查询。
线段树的应用非常广泛,在离线查询、区间统计、区间更新等问题中有着重要的作用。
一、概念线段树是一颗二叉树,其中每个节点代表了一个区间。
根节点表示整个待查询区间,而叶子节点表示的是单个元素。
每个内部节点包含了其子节点所代表区间的并集。
二、构建线段树线段树的构建过程是自底向上的。
将待查询数组划分成一颗满二叉树,并将每个区间的和存储在相应的节点中。
对于叶子节点,直接存储对应元素的值。
而非叶子节点的值可以通过其子节点的值计算得到。
三、线段树的查询对于区间查询操作,可以通过递归方式实现。
从根节点开始,判断查询区间和当前节点所代表的区间是否有交集。
若没有交集,则返回当前节点的默认值。
若查询区间包含当前节点所代表的区间,则返回当前节点存储的值。
否则,将查询区间分割成左右两部分继续递归查询。
四、线段树的更新对于区间更新操作,也可以通过递归方式实现。
与查询操作类似,首先判断查询区间和当前节点所代表的区间的关系。
若没有交集,则无需更新。
若查询区间包含当前节点所代表的区间,则直接更新当前节点的值。
否则,将更新操作分割成左右两部分继续递归更新。
五、应用案例:区间最值查询一个常见的线段树应用是求解某个区间的最值。
以查询区间最小值为例,可以通过线段树来高效地解决。
首先构建线段树,然后进行区间查询时,分为以下几种情况处理:若当前节点所代表的区间完全包含于查询区间,则直接返回该节点的值;若当前节点所代表的区间与查询区间没有交集,则返回默认值;否则,将查询区间分割成左右两部分继续递归查询,最后返回两个子区间查询结果的较小值。
六、总结线段树是一种非常有用的数据结构,能够高效地解决区间查询问题。
通过合理的构建和操作,线段树可以应用于多种场景,如区间最值查询、离线查询等。
熟练掌握线段树的概念和应用方法,对解决问题具有重要意义。
浅谈线段树原理及实现
浅谈线段树原理及实现⼤家好,给⼤家介绍完了树状数组(有兴趣的读者可以在我的博客⽂章中阅读),现在来给⼤家介绍另⼀种数据结构——线段树。
它们结构都有共同点,但是线段树更为复杂,功能也更为强⼤,接下来就会⼀步⼀步向你介绍线段树的功能和⽤法。
线段树(Segment Tree)的简介:线段树是⼀种⼆叉搜索树,它将⼀个区间划分成⼀些单元区间,每个单元区间对应线段树中的⼀个叶结点,它基于分之思想,⽤于在线性区间上完成动态统计,它的思想主要是将线性的区间递归划分成长度相同的两段,直到叶⼦节点,每个叶⼦表⽰⼀个数据,向上每⼀层⽗亲节点都涵盖了所有⼉⼦节点,模型图如下:在图上,我们可以看出,它的根节点即为我们需要修改和统计的线段,它将其划分成两段⼦线段,运⽤了⼆分的思想mid=(l+r)/2,我们再来想想看⼀个节点应该包含什么数据,⾸先肯定是它的范围L和R,在就是⼀个数据域date,⾄于这个date存储什么数据,那就根据需要⽽定,例如可以存储这⼀段的最值,也可存储这⼀段的和等等,当然也可以都存储,⽆⾮就是多⼏个变量,基本的节点信息就是这些,然后再考虑⼀下⽤多⼤的数组来存储,根据上图,我们可以发现由于是⼆分的思想,所以不⼀定是理想的满⼆叉树,⼀个节点可能会有空的⼦节点,N个节点的满⼆叉树节点为N+N/2+N/4+...+1=2*N-1,然后再下⾯⼀层节点数为2N,要空余出来,所以存储1~N数组要开4*N,可以⽤⼀下代码表⽰:struct Segment_tree{int l,r;int date;}node[SIZE*4];由此我们可以总结⼀下线段树节点的特点:线段树每个节点代表着⼀个区间。
线段树的根节点表⽰统计范围区间1~n。
线段树每个叶⼦节点代表着⼀个数值。
对于每个除叶⼦节点外的节点[L,R],它的左⼦节点[L,mid],右⼦节点[mid+1,R]。
对于编号为i的节点,左⼦节点编号2*i,右⼦节点编号2*i+1。
菜鸟都能理解的线段树入门经典
菜鸟都能理解的线段树入门经典线段树的定义首先,线段树既是线段也是树,并且是一棵二叉树,每个结点是一条线段,每条线段的左右儿子线段分别是该线段的左半和右半区间,递归定义之后就是一棵线段树,图示如下图1.线段树示意图定义线段树的数据结构struct Line{int left, right, count;Line *leftChild, *rightChild;Line(int l, int r): left(l), right(r) {}};PS:其中的count字段表示该条线段有几条明白了线段树的定义之后,我们来举一个例子来说明线段树的应用例题:给定N条线段,{[2, 5], [4, 6], [0, 7]}, M个点{2, 4, 7},判断每个点分别在几条线段出现过看到题目,有的人第一感觉想出来的算法便是对于每一个点,逐个遍历每一条线段,很轻松地判断出来每个点在几条线段出现过,小学生都会的算法,时间复杂度为O(M*N)如国N非常大,比如2^32-1, M也非常大M = 2^32 - 1, O(M*N)的算法将是无法忍受的,这个时候,线段树便隆重登场了线段树的解法:1.首先,我们找出一个最大的区间能够覆盖所有的线段,遍历所有的线段,找线段的最值(左端点的最小值,右端点的最大值)便可以确定这个区间,对于{[2, 5], [4, 6], [0, 7]}, 这个区间为[0, 7],时间复杂度为O(N)2.然后,根据此区间建一棵线段树(见图1), 时间复杂度为O(log(MAX-MIN))3.对于每一条线段A,从根节点开始遍历这棵线段树,对于每一个当前遍历的结点NODE(其实线段树中每一个结点就是一条线段),考虑三种情况a)如果线段A包含在线段NODE的左半区间,那么从NODE的左儿子(其实就是NODE 的左半区间)开始遍历这棵树b)如果线段A包含在线段NODE的右半区间,那么从NODE的右儿子(其实就是NODE 的右半区间)开始遍历这棵树c)如果线段A刚好和线段NODE重合,停止遍历,并将NODE中的count字段加1d)除了以上的情况,就将线段A的左半部分在NODE的左儿子处遍历,将A的右半部分在NODE的右儿子处遍历补充说明:对于以上的步骤,所做的工作其实就是不断地分割每一条线段,使得分割后的每一条小线段刚好能够落在线段树上,举个例子,比如要分割[2, 5],首先将[2, 5]和[0, 7]比较,符合情况d, 将A分成[2, 3]与[4, 5]I)对于[2, 3]从[0, 7]的左半区间[0, 3]开始遍历将[2, 3]与[0, 3]比较,满足情况b,那么从[0, 3]的右半区间[2, 3]开始遍历,发现刚好重合,便将结点[2, 3]count字段加1II)对于[4, 5]从[0, 7]的右半区间[4, 7]开始遍历将[4, 5]与[4, 7]比较,满足情况b,从[4, 7]的左半区间[4, 5]开始遍历,发现刚好重合,便将结点[4, 5]count字段加1于是对于[2, 5]分割之后线段树的情况为图2图2.分割[2,5]之后线段树的情况显然,我们看到,上述的遍历操作起始就是将[2, 5]按照线段树中的线段来分割,分割后的[2, 3]与[4, 5]其实是与[2, 5]完全等效的最后,我们将剩下的两条线段按照同样的步骤进行分割之后,线段树的情况如下图3这一步的时间复杂度为O(N*log(MAX-MIN))4.最后,对于每一个值我们就可以开始遍历这一颗线段树,加上对于结点的count字段便是在线段中出现的次数比如对于4,首先遍历[0, 7],次数= 0+1=1;4在右半区间,遍历[4, 7],次数= 1+0=0;4在[4, 7]左半区间, 次数= 1+2=3;4在[4, 5]左半区间,次数= 3+0 = 4,遍历结束,次数= 3说明4在三条线段中出现过,同理可求其他的值,这一步的时间复杂度为O(M*log(MAX-MIN))最后,总的时间复杂度为O(N)+O(log(MAX-MIN))+O(N*log(MAX-MIN))+(M*log(MAX-MIN)) = O((M+N)*log(MAX-MIN))由于log(MAX-MIX)<=64所以最后的时间复杂度为O(M+N)最后,放出源码[cpp] view plaincopy#include <iostream>using namespace std;struct Line{int left, right, count;Line *leftChild, *rightChild;Line(int l, int r): left(l), right(r) {}};//建立一棵空线段树void createTree(Line *root) {int left = root->left;int right = root->right;if (left < right) {int mid = (left + right) / 2;Line *lc = new Line(left, mid);Line *rc = new Line(mid + 1, right);root->leftChild = lc;root->rightChild = rc;createTree(lc);createTree(rc);}}//将线段[l, r]分割void insertLine(Line *root, int l, int r) {cout << l << " " << r << endl;cout << root->left << " " << root->right << endl << endl;if (l == root->left && r == root->right) {root->count += 1;} else if (l <= r) {int rmid = (root->left + root->right) / 2;if (r <= rmid) {insertLine(root->leftChild, l, r);} else if (l >= rmid + 1) {insertLine(root->rightChild, l, r);} else {int mid = (l + r) / 2;insertLine(root->leftChild, l, mid);insertLine(root->rightChild, mid + 1, r);}}}//树的中序遍历(测试用)void inOrder(Line* root) {if (root != NULL) {inOrder(root->leftChild);printf("[%d, %d], %d\n", root->left, root->right, root->count);inOrder(root->rightChild);}}//获取值n在线段上出现的次数int getCount(Line* root, int n) {int c = 0;if (root->left <= n&&n <= root->right)c += root->count;if (root->left == root->right)return c;int mid = (root->left + root->right) / 2;if (n <= mid)c += getCount(root->leftChild, n);elsec += getCount(root->rightChild, n);return c;}int main() {int l[3] = {2, 4, 0};int r[3] = {5, 6, 7};int MIN = l[0];int MAX = r[0];for (int i = 1; i < 3; ++i) {if (MIN > l[i]) MIN = l[i];if (MAX < r[i]) MAX = r[i];}Line *root = new Line(MIN, MAX);createTree(root);for (int i = 0; i < 3; ++i) {insertLine(root, l[i], r[i]);}inOrder(root);int N;while (cin >> N) {cout << getCount(root, N) << endl; }return 0;}。
线段树模板讲解
线段树模板讲解洛⾕题⽬链接:线段树是⼀种⽤于区间修改查询的数据结构,可以⽀持的操作有单点修改区间查询,区间修改单点查询,区间修改区间查询等.线段树有递归版和结构体版,递归版在处理⼀开始没有赋初始值的问题时可以不⽤建树,⽽结构体版的则显得⽐较条理清晰.线段树⽐树状数组的代码复杂的多,但是树状数组多⽀持⼀个区间修改区间查询的操作,并且可以与其他的⼀些数据结构相适应(像是树链剖分等),也有很多的在此基础上的提⾼的算法,线⾯开始讲解⼀下思路.线段树操作如下:输⼊点权,建树.进⾏修改和查询.建树:在线段树中建树⼀般采⽤的是递归的⽅式建树,在建树的过程中保留每个点的信息(区间左端点,右端点,区间和等),在线段树中的每个节点就是⼀个区间.代码如下:void build(lol root,lol left,lol right){//节点,节点左端点,节点右端点if(left==right){//当节点左端点等于右端点时,即为叶⼦节点sum[root]=w[left];//叶⼦节点的区间和即为点权return;//这⾥记得要退出回溯}build(root*2,left,mid);//左⼦树build(root*2+1,mid+1,right);//右⼦树递归建树sum[root]=sum[ll(root)]+sum[rr(root)];//回溯时收集区间和}那么这样建好的树就会有这样的性质:当前节点的左右⼉⼦分别是root*2和root*2+1当前节点包括的范围刚好是左右⼉⼦的区间的并集所以之后在进⾏修改,查询等操作时会很⽅便.下移懒惰标记:这⾥提及了⼀个重要的操作:lazy[]数组,懒惰标记.lazy数组⽤于保存修改在某个区间上的值,但不即时修改赋值到节点上,⽽是在之后查询时需要查询它⼦树的值的时候再将懒惰标记下移.那么下移lazy标记就是将当前区间的修改细化到每个⼩区间上.具体细节见代码注释:void pushdown(int root,int left,int right){lazy[root*2]+=lazy[root];//左右⼦树加上上⾯节点的标记lazy[root*2+1]+=lazy[root];sum[root*2]+=lazy[root]*(mid-left+1);//将区间和加上⼦树的个数乘标记的⼤⼩sum[root*2+1]+=lazy[root]*(right-mid);lazy[root]=0;//消去节点的懒惰标记}为什么修改区间和时要将左⼦树乘上(mid-left+1)呢?因为左⼦树的左端点是left,右端点是mid,那么正好(mid-left+1)就是左⼦树的节点数,乘上lazy[root]的值也就是将它的⼦树每个加上lazy[root]的值.修改:修改时是寻找⼀个能全部包含于修改范围的区间,并将它打上lazy标记.⼀个需要修改的范围可以看做是⼀个个⼩的范围的集合,如:1到8的区间可以看做是[1,5]和[6,8];5到6的区间可以看做是[5,5]和[6,6];如果是将1~8加5,则将区间[1,5]和区间[6,8]节点的懒惰标记加上5就可以了.代码如下:1void updata(int root,int left,int right,int l,int r,int val){2if(l<=left&&right<=r){//如果找到⼀个能全部被包含于修改范围的区间3 lazy[root]+=val;//则加上懒惰标记4 sum[root]+=val*(right-left+1);//同时也要修改区间和,与pushdown的修改同理5return;6 }7if(lazy[root]) pushdown(root,left,right);//修改时也要将之前的懒惰标记下移8if(l<=mid) updata(ll(root),left,mid,l,r,val);9if(mid<r) updata(rr(root),mid+1,right,l,r,val);//递归寻找能全被包含的区间10 sum[root]=sum[ll(root)]+sum[rr(root)];11 }查询:查询和修改操作⽐较像,也是将⼀个⼤区间细化为⼀个个⼩区间,找能被完全包含的区间进⾏查询.下⾯直接上代码:1 lol query(int root,int left,int right,int l,int r){2if(l<=left&&right<=r) return sum[root];//完全被包含的区间直接返回区间和3if(r<left||right<l) return0;//如果区间和查找区间没有交集,则直接返回04if(lazy[root]) pushdown(root,left,right);//这⾥要写在判断是否与查找区间有交集后⾯5return query(ll(root),left,mid,l,r)+query(rr(root),mid+1,right,l,r);//递归查询6 }为什么要把懒惰标记下移的操作放在判断后呢?我们举个例⼦,假设已经递归到了叶⼦节点,如果先下移标记,就有可能导致数组的越界(向下移标记时左右⼦树的节点标号都是节点的两倍),所以要先进⾏判断区间的操作.⼏个简单的操作讲完了,下⾯放⼀个完整模板:1 #include<bits/stdc++.h>2#define mid (left+right>>1)3#define ll(x) (x<<1)4#define rr(x) (x<<1|1)5using namespace std;6 typedef long long lol;7const int N=500000;89 lol n,m;10 lol w[N+10];11 lol sum[N+10];12 lol lazy[N+10];1314int gi(){15int ans=0,f=1;char i=getchar();16while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();}17while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}18return ans*f;19 }2021void build(lol root,lol left,lol right){22if(left==right){23 sum[root]=w[left];24return;25 }26 build(ll(root),left,mid);27 build(rr(root),mid+1,right);28 sum[root]=sum[ll(root)]+sum[rr(root)];29 }3031void pushdown(int root,int left,int right){32 lazy[ll(root)]+=lazy[root];33 lazy[rr(root)]+=lazy[root];34 sum[ll(root)]+=lazy[root]*(mid-left+1);35 sum[rr(root)]+=lazy[root]*(right-mid);36 lazy[root]=0;37 }3839void updata(int root,int left,int right,int l,int r,int val){40if(l<=left&&right<=r){41 lazy[root]+=val;42 sum[root]+=val*(right-left+1);43return;44 }45if(lazy[root]) pushdown(root,left,right);46if(l<=mid) updata(ll(root),left,mid,l,r,val);47if(mid<r) updata(rr(root),mid+1,right,l,r,val);48 sum[root]=sum[ll(root)]+sum[rr(root)];49 }5051 lol query(int root,int left,int right,int l,int r){52if(l<=left&&right<=r) return sum[root];53if(r<left||right<l) return0;54if(lazy[root]) pushdown(root,left,right);55return query(ll(root),left,mid,l,r)+query(rr(root),mid+1,right,l,r);56 }5758int main(){59int x,y,val,flag;60 n=gi();m=gi();61for(int i=1;i<=n;i++) w[i]=gi();62 build(1,1,n);63for(int i=1;i<=m;i++){64 flag=gi();65if(flag==1){66 x=gi();y=gi();val=gi();67 updata(1,1,n,x,y,val);68 }69if(flag==2){70 x=gi();y=gi();71 printf("%lld\n",query(1,1,n,x,y));72 }73 }74return0;75 }另外再贴⼀个结构体版的:#include<bits/stdc++.h>#define ll(x) (x<<1)#define rr(x) (x<<1|1)using namespace std;const int N=400000+5;typedef long long lol;lol n, m;lol w[N];struct seg_tree{lol sum, l, r, lazy;}t[N];lol gi(){lol ans = 0 , f = 1; char i=getchar();while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();}while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();} return ans * f;}void up(lol root){t[root].sum = t[ll(root)].sum + t[rr(root)].sum;}void build(lol root,lol l,lol r){int mid = l+r>>1;t[root].l = l , t[root].r = r;if(l == r){t[root].sum = w[l];return;}build(ll(root),l,mid);build(rr(root),mid+1,r);up(root);}void pushdown(lol root){lol mid = t[root].l + t[root].r >> 1;t[ll(root)].lazy += t[root].lazy;t[rr(root)].lazy += t[root].lazy;t[ll(root)].sum += t[root].lazy*(mid-t[root].l+1);t[rr(root)].sum += t[root].lazy*(t[root].r-mid);t[root].lazy = 0;}void updata(lol root,lol l,lol r,lol val){lol mid = t[root].l+t[root].r>>1;if(l<=t[root].l && t[root].r<=r){t[root].sum += val * (t[root].r-t[root].l+1);t[root].lazy += val;return;}if(t[root].lazy) pushdown(root);if(l <= mid) updata(ll(root),l,r,val);if(mid < r) updata(rr(root),l,r,val);up(root);}lol query(lol root,lol l,lol r){if(l<=t[root].l && t[root].r<=r) return t[root].sum;if(r<t[root].l || t[root].r<l) return0;if(t[root].lazy) pushdown(root);return query(ll(root),l,r)+query(rr(root),l,r);}int main(){lol f, x, y, val; n = gi(); m = gi();for(lol i=1;i<=n;i++) w[i] = gi();build(1,1,n);for(lol i=1;i<=m;i++){f = gi(); x = gi(); y = gi();if(f == 1) val = gi() , updata(1,x,y,val);else printf("%lld\n",query(1,x,y));}return0;}。
线段树入门
小结
ቤተ መጻሕፍቲ ባይዱ
其实线段树就是用某个值来代替(概括) 一段区间的值,来避免求解时用到大量的 基础元素。
框架:建树
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;
线段树应用PPT课件
线段树与数组的比较
总结词
线段树在处理复杂查询时优 于数组
详细描述
数组虽然便于随机访问,但 在处理复杂查询如区间最大 值、最小值等问题时,线段 树能够提供更快的查询速度
。
总结词
线段树在处理动态数据集时更具优势
详细描述
当数据集频繁变动时,线段树能够快速地 更新和维护数据,而数组可能需要重新构 建或采取其他复杂的操作。
空间效率
线段树通过节点间的关系,将大 问题分解为小问题,所需空间相 对较小。
线段树的缺点
01
02
03
节点分裂与合并
在线段树进行插入、删除 等操作时,可能会导致节 点分裂或合并,使得树的 平衡性难以维护。
构造与重建
线段树在处理大规模数据 时,可能需要多次重建, 导致时间复杂度较高。
适用场景限制
线段树适用于区间查询问 题,对于其他类型的问题 可能需要其他数据结构或 算法。
线段树应用ppt课件
目录
• 引言 • 线段树的原理 • 线段树的应用实例 • 线段树与其他数据结构的比较 • 线段树的优缺点分析 • 总结与展望
01 引言
什么是线段树
定义
线段树是一种用于处理区间查询问题 的数据结构,它可以在线段上高效地 执行查询、更新和删除操作。
结构
线段树通常由一个根节点和若干个子 节点组成,每个节点包含一个区间的 信息,并且每个节点与其子节点之间 存在一一对应关系。
总结词
数组在随机访问和存储空间方面更具优势
详细描述
数组能够提供快速的随机访问,并且在相 同的数据量下,数组所需的存储空间可能 比线段树更少。
05 线段树的优缺点分析
线段树的优点
高效区间查询
简单线段树知识点详解
简单线段树知识点详解简单线段树知识点详解本篇随笔讲解信息学奥林匹克竞赛中强⼤且常⽤的猛⼠数据结构——线段树。
因为线段树博⼤精深,有许多变形和应⽤⽅式。
区区⼀篇随笔是绝对⽆法尽叙的。
所以在这⾥笔者只为读者讲解简单线段树。
希望每⼀位有缘读到这篇随笔的⼈都能对线段树有⼀个深刻的理解,并会解决线段树的简单问题。
由于线段树属于⼀种⾼级数据结构。
所以在学习线段树的时候需要的知识铺垫⽐较多。
建议读者先对树状结构、⼆分以及递归编程法有深刻的认识和理解,然后再进⾏线段树的学习。
这样的话会⽅便很多。
当然,如果你缺少了前述铺垫知识的⼀项或⼏项,也并不代表你⼀定学不好线段树。
勇于尝试、敢于挑战、努⼒思考。
这会对你线段树及以后很多知识点的学习有极⼤的促进作⽤。
那我就开始了。
线段树的概念在介绍线段树的概念之前,我先介绍线段树的⽤途:线段树⼀般⽤于区间统计。
即统计[x,y]区间内的某⼀个特性。
这个特性可以有很多,⽐如区间求和,区间最值等等。
定义什么的特别复杂,我们争取⽤⼀张图搞清楚对线段树的直观理解。
上图是⼀棵1-5区间的线段树。
我们发现这个线段树是⼀棵⼆叉树,每个节点表⽰⼀个区间,根节点对应区间1-n.每个叶⼦节点都只表⽰单点,针对⼆叉树编号的性质(⼆叉树的每个⽗亲节点f的左节点编号是2f,右节点编号是2f+1),我们可以使⽤⼀维数组实现线段树。
也就是说,我们开⼀个⼀维数组,⼀维数组的下标表⽰这棵线段树的节点编号,⾥⾯存的值表⽰这个节点所表⽰的区间中我们要维护的特性:如和、最值等。
简单线段树⽀持的操作刚刚已经说过,线段树是⼀种博⼤精深的数据结构,它的功能和操作实在是太多了。
之所以反复强调这些,是为了让读者清楚,在线段树的海洋中,我们都不过是探其⼀⾓罢了,千万不要妄⾃尊⼤,以为⾃⼰已经把线段树全部搞完了。
简单线段树⽀持单点查询,区间查询,单点修改,区间修改,我们发现这和树状数组的⼀些⽀持项⽬类似,但是却不完全包含,因为树状数组仅⽀持区间求和,且必须是1−n的求和,如果想要[x,y]的任意区间求和的话,必须需要使⽤差分思想来相减。
线段树讲解(数据结构、C++)
线段树讲解(数据结构、C++)声明:仅⼀张图⽚转载于,⾃⼰画太⿇烦了。
那个博客的讲解也很好,只是他⽤了指针的⽅式来定义线段树,⽽我⽤了结构体,并且他讲了线段树的更⾼级的操作,若对线段树的初级操作不理解,请继续阅读线段树作为⼀种⼗分常⽤的数据结构,在NOIP、NOI中⼴泛的出现,所以在这⾥对线段树进⾏简单的讲解。
线段树⽀持对⼀个数列的求和、单点修改、求最值(最⼤、最⼩)、区间修改(需要lazy标记,暂不讲解)。
这⼏种操作,时间复杂度是(logn)级别的,是⼀种⼗分优秀的数据结构。
因此其获得了⼴泛的应⽤。
定义:顾名思义,它是⼀种树形结构,但每段不是平常所学的⼀个点⼀个点的树,⽽是⼀条⼀条的线段,每条线段包含着⼀些值,其中最主要的是起始和结束点记作 l,r 即左端点和右端点。
那么该如何划分线段树呢?我们采⽤⼆分的思想,即每次将⼀段取半,再进⾏接下来的操作,这样综合了操作的⽅便程度和时间复杂度。
因为线段树通过⼆分得来,所以线段树是⼀颗⼆叉树。
这也⽅便了对⼉⼦查找。
下⾯是线段树的图,有利于理解:建树:仅仅知道模型还是不够的,建树的过程是线段树的关键(build(1,1,n))从⼀号开始,左端是1,右端是n位运算 i<<1 等效于 i/2 (i<<1)|1 等效于 i/2+1 加速。
inline void update(int i)更新i节点维护的值(求和,最⼤……){node[i].sum=node[i<<1].sum+node[(i<<1)|1].sum;node[i].maxx=max(node[i<<1].maxx,node[(i<<1)|1].maxx);}inline void build(int i,int l,int r)//inline 还是加速{node[i].l=l;node[i].r=r;//左右端点为当前递归到的 l 和 rif(l==r){//若l==r 则当前的树节点是真正意义上的点node[i].maxx=a[l];//最⼤值就是本⾝的值node[i].sum=a[l];//区间的和就是本⾝的值return;}int mid=(l+r)/2;//因为是⼆叉树所以以中点为分割点build(i<<1,l,mid);//根据⼆叉树的知识,左⼉⼦是i/2右⼉⼦是i/2+1build((i<<1)|1,mid+1,r);update(i);}数列求和:这是线段树的⼀个典型算法,其他的很多应⽤都是从中转化的。
线段树
线段树转载请注明出处,谢谢!/metalseed/article/details/8039326持续更新中···一:线段树基本概念1:概述线段树,类似区间树,是一个完全二叉树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(lgN)!性质:父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树需要的空间为数组大小的四倍2:基本操作(demo用的是查询区间最小值)线段树的主要操作有:(1):线段树的构造void build(int node, int begin, int end);主要思想是递归构造,如果当前节点记录的区间只有一个值,则直接赋值,否则递归构造左右子树,最后回溯的时候给当前节点赋值1.#include <iostream>ing namespace std;3.4.const int maxind = 256;5.int segTree[maxind * 4 + 10];6.int array[maxind];7./* 构造函数,得到线段树 */8.void build(int node, int begin, int end)9.{10. if (begin == end)11. segTree[node] = array[begin]; /* 只有一个元素,节点记录该单元素 */12. else13. {14. /* 递归构造左右子树 */15. build(2*node, begin, (begin+end)/2);16. build(2*node+1, (begin+end)/2+1, end);17.18. /* 回溯时得到当前node节点的线段信息 */19. if (segTree[2 * node] <= segTree[2 * node + 1])20. segTree[node] = segTree[2 * node];21. else22. segTree[node] = segTree[2 * node + 1];23. }24.}25.26.int main()27.{28. array[0] = 1, array[1] = 2,array[2] = 2, array[3] = 4, array[4] = 1, array[5] = 3;29. build(1, 0, 5);30. for(int i = 1; i<=20; ++i)31. cout<< "seg"<< i << "=" <<segTree[i] <<endl;32. return 0;33.}此build构造成的树如图:(2):区间查询int query(int node, int begin, int end, int left, int right);(其中node为当前查询节点,begin,end为当前节点存储的区间,left,right为此次query所要查询的区间)主要思想是把所要查询的区间[a,b]划分为线段树上的节点,然后将这些节点代表的区间合并起来得到所需信息比如前面一个图中所示的树,如果询问区间是[0,2],或者询问的区间是[3,3],不难直接找到对应的节点回答这一问题。
线段树——精选推荐
线段树 导⼊概念:1>线段树的结构:线段树是⼀种⼆叉搜索树。
2>⽤途: 1.在线维护修改以及查询区间上的最值,求和。
2.扩充到⼆维线段树(矩阵树)和三维线段树(空间树)。
3>时间复杂度: 对于⼀维线段树来说,每次更新以及查询的时间复杂度为O(logN)。
线段树基本内容 对于A[1:6] = {1,8,6,4,3,5}来说,线段树如上所⽰,红⾊代表每个结点存储的区间,蓝⾊代表该区间最值。
可以发现,每个叶⼦结点的值就是数组的值,每个⾮叶⼦结点的度都为⼆,且左右两个孩⼦分别存储⽗亲⼀半的区间。
每个⽗亲的存储的值也就是两个孩⼦存储的值的最⼤值。
对于⼀个区间[l,r]来说,最重要的数据当然就是区间的左右端点l和r,但是⼤部分的情况我们并不会去存储这两个数值,⽽是通过递归的传参⽅式进⾏传递。
这种⽅式⽤指针好实现,定义两个左右⼦树递归即可,但是指针表⽰过于繁琐,⽽且不⽅便各种操作,⼤部分的线段树都是使⽤数组进⾏表⽰,那这⾥怎么快速使⽤下标找到左右⼦树呢。
对于上述线段树,我们增加绿⾊数字为每个结点的下标 注意:⽆优化的线段树建树需要2*2k(2k-1 < n < 2k)空间,⼀般会开到4*n的空间防⽌RE。
易知:1.每个左⼦树的下标都是偶数,右⼦树的下标都是奇数且为左⼦树下标+1 2. l= fa*2 3.r = fa*2+1 推论:以k为⽗节点的编号有:左⼦树编号为k<<1,右⼦树编号为k<<1|1; code:线段树的基本操作 1.点更新 更新⼀个⼦节点,则每个包含此值的结点都需要更新,于是我们可以⽤递归查找哪个⼦节点需要修改,在回溯的时候改变⽗节点的值。
code: 2.区间查询 我们可以将要查询的区间分配到个个节点中,在递归的同时判断所要查询的区间是否能够完全包含当前节点所代表的区间 1.完全包含,返回该节点上的值。
2.不完全包含,将该查询任务下发到左右⼦节点中。
[PKU 2777] 线段树(一) {概述 基本操作}
[PKU 2777] 线段树(一) {概述基本操作}{以前写的线段树都是零碎而且描述的也不清楚最近打算整理一下就从我的第一个线段树程序开始吧}线段树Segment_tree网上有人把线段树翻译成Interval_TreeInterval_Tree 是另外一种数据结构而且并非二叉树这个是线段树的标准E文翻译可以看wikipedia的原文/wiki/Segment_tree 顾名思义线段树存储的是连续的线段而非离散的节点先看一张经典的线段树图解这个就是标准的线段树既然是树形结构我们就得先考虑怎么存储这棵树分析线段树的定义*线段树是一棵二叉树记为T(a, b)*参数a,b表示区间[a,b] 其中b-a称为区间的长度记为L *线段树T(a,b)也可递归定义为-若L>1 [a, (a+b) div 2]为T的左儿子[(a+b) div 2,b]为T的右儿子-若L=1 T为叶子节点可以得到一些基本性质*线段树除最后一层外是满二叉树*线段树是平衡的高度是Log2L左右如此我们有2种存储方法*直接用指针定义节点type node=recordls,rs:^node;l,r:longint;end;其中ls rs分别为左右儿子l,r是区间的范围真正实现时一般用数组模拟指针我们只需定义longint数组ls[] rs[] l[] r[]*用*2和*2+1代替左右儿子指针由于是除最后一层外是满二叉树我们可以向存储堆一样存储线段树用l[] r[]来存储节点区间范围x的左右儿子分别就是x*2和x*2+1具体实现用位移代替乘2这样乘法指针运算和上述数组调用一样几乎不需要时间具体用哪种纯粹是个人喜好没什么区别(下文中我的程序都是用的数组模拟直接存储儿子指针)接下来讨论线段树的具体操作也就是维护这种数据结构的算法(srO 数据结构+算法=程序Orz)总结起来就两个词递归& 分治结合一个具体问题吧PKU 2777/problem?id=2777来源:POJ2777【问题描述】一个有L个格子的染色板,每个格子编号为1,2,3……L,每个格子初始都染成了1号色。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
线段树入门(zz)Posted on 2010-08-29 21:22 MiYu阅读(184) 评论(0)编辑收藏引用所属分类: ACM_资料、ACM ( 数据结构)从简单说起,线段树其实可以理解成一种特殊的二叉树。
但是这种二叉树较为平衡,和静态二叉树一样,都是提前已经建立好的树形结构。
针对性强,所以效率要高。
这里又想到了一句题外话:动态和静态的差别。
动态结构较为灵活,但是速度较慢;静态结构节省内存,速度较快。
接着回到线段树上来,线段树是建立在线段的基础上,每个结点都代表了一条线段[a , b]。
长度为1的线段成为元线段。
非元线段都有两个子结点,左结点代表的线段为[a , (a + b ) / 2],右结点代表的线段为[( a + b ) / 2 , b]。
图一就是一棵长度范围为[1 , 10]的线段树。
长度范围为[1 , L] 的一棵线段树的深度为log ( L - 1 ) + 1。
这个显然,而且存储一棵线段树的空间复杂度为O(L)。
线段树支持最基本的操作为插入和删除一条线段。
下面已插入为例,详细叙述,删除类似。
将一条线段[a , b] 插入到代表线段[l , r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。
如果a<mid,那么将线段[a , b] 也插入到p的左儿子结点中,如果b>mid,那么将线段[a , b] 也插入到p的右儿子结点中。
插入(删除)操作的时间复杂度为O ( Log n )。
上面的都是些基本的线段树结构,但只有这些并不能做什么,就好比一个程序有输入没输出,根本没有任何用处。
最简单的应用就是记录线段有否被覆盖,并随时查询当前被覆盖线段的总长度。
那么此时可以在结点结构中加入一个变量int count;代表当前结点代表的子树中被覆盖的线段长度和。
这样就要在插入(删除)当中维护这个count值,于是当前的覆盖总值就是根节点的count值了。
另外也可以将count换成bool cover;支持查找一个结点或线段是否被覆盖。
例题1(ZJU1610 Count The Colors线段树基本应用题目)给出在线段[0,8000]上的若干次涂色,问最后能看见哪些颜色,并统计能看到多少段。
解析就这个题目而言,方法很多,而且数据范围不大,但我们由线段树的角度来解决这个问题。
建立一棵代表线段[0,8000]的线段树,涂色操作就是将[a , b]涂成颜色c。
最后做统计。
结构如下:struct TNode {int left , right;int col;TNode *LeftChild , *RightChild;};col 有几种情况,如果col为-1,代表了尚未涂色,-2代表了是混和色,就是说这条线段并不是单一的颜色。
其他情况,便是这条线段都是这个颜色的了。
全部程序见附录1。
线段树的第一种变化基本的线段树代表的是线段,如果我们把线段离散成点来看,那么线段树可以变化成一种离散类型线段树。
这里可以有两种理解。
一种离散关系可以是连续的线段的点,比方说在一条直线上放置的连续小球的着色问题;另一种则是完全将线段离散化分成若干小段,对每一段小段做为元线段来建立线段树,这种线段树可以支持实数划分类型的线段。
例题2(ZJU2451 Minimizing maximizer )Andy想要得到一组数中的最大值。
会有一系列的操作Sorter(i[1], j[1]), ...,Sorter (i[k], j[k])。
作用是将数组中的第i[k]个数字到第j[k]个数字排序。
按照输入给出的顺序,你可以选择要不要执行这个操作。
问题是最少需要多少步操作,可以求出这个最大值。
题目保证可以求出。
多组数据。
第一行为两个数字N,M。
表示N个数,M个操作。
接下来M行,每行描述一个操作i[k] , j [k]。
对于每组数据,输出最少需要多少次操作分离得到最大值。
每组数据一行。
解析由于要将最大的数字分离到最后的一位,如果我们考虑将数组看成一条[1,n]的线段,而每项操作也看成是从[i[k],j[k]]的线段,那么就是要求按照输入的顺序,将线段[1,n]从左到右依次覆盖掉,问题变成求最小的覆盖线段总数。
考虑最基本的规划方法,用Opt [k] 表示覆盖掉 [1,k]的线段最少需要的步数,那么状态转移方程为:Opt [k] = min { Opt [d] + 1 | j [p] = k && d >= i [p] && d <= j [p] && k > 1 }Opt [1] = 0;最后的答案就是Opt [n]了,但是考虑时间复杂度,是O(m^2)级别的,m最大为500000,超时无疑。
但是这里我们看到了规划的决策集合是一条连续的线段,是要在这条线段上面取得最小值,那么线段树的结构就正好适合在这里应用了。
由于这里最小的单位是一个点,所以我们采取线段树的第一种变化,把元线段设置为单位点,即[k,k]。
在规划的时候维护线段树即可。
线段树结点结构中,需要加入的元素是int minstep 代表最少需要用到的覆盖线段数目可以覆盖到当前结点所代表的线段中。
全部程序见附录2。
例题3(PKU2104K-th Number)给出一个大小为n的数组A[],给出m个问题(1 <= n <= 100 000, 1 <= m <= 5 000)。
问题格式为Q(i,j,k),询问从A[i]到A[j]第k大的元素是什么。
A[]中的数各不相同。
解析由于仍旧是离散的整数问题,我们依旧采取第一种变化。
看到题目,最基本的想法就是排序然后求第k个数了,但是时限上不能满足要求。
线段树的最强大方面就是将一组数(一条线段)放到一起处理。
每层树需要的线段数目不会超过4,而深度为logn,所以最后操作的复杂度会是O(logn)。
但是仅仅应用线段树还是不够的,即使我们知道了需要处理的线段是那些,但是由于线段过多,也无法准确求出第k个元素究竟是什么。
这里二分策略就派上了用场。
我们用二分枚举第k个数字P,然后再在所要的线段中找到枚举的P所在的位置,同样是用二分的策略。
所以复杂度是O(mlognlognlogn)。
我们在找P所在的位置的时候需要用到二分策略,也就是说,我们需要将线段所代表的结点排序,这里可以将每一层的所有数放到一起,统一成一个数组SortArray[depth][]。
其实也可以理解成将归并排序的每个步骤记录下来。
全部程序见附录3。
线段树的第二种变化(树状数组)在结构上对线段树进行改变,可以得到线段树的另一种变化。
用O(n)的一维数组构造出线段树,无其他附加空间。
比方说,一棵从[0,L]的线段树表示为TNode Tree[L];这里应用二进制将树进行划分。
将整数R的二进制表示中的最后一个1换成0,得到数L。
Tree[R]代表的线段就是[L,R]。
例如:6的二进制表示为(110)2将最后一个1换成0即为(100)2=4,所以Tree[6]代表的线段就是[4,6]。
析出数R的最后一位1的方法是:LowBit(R)=R^~R。
包含点L的一系列数为x1,x2,……,这里x1=R,x2=x1+LowBit (x1),x3=x2+LowBit(x2),……这种线段树的优点在于:1.节省空间。
完全线段长度的空间,无需左右指针,无需左右范围。
2.线段树查找严格log(R),因为二进制的每位查找一遍。
3.状态转移快,操作简单。
4.扩展到多维较为容易。
缺点:1.随意表示线段[a,b]较为困难。
这种线段树适用于:1.查找线段[0,L]的信息。
2.求线段[a,b]的和(应用部分和做差技术)。
// problem zju 1610// Segment Tree#define NoColor -1#define MulColor -2#include <stdio.h>#include <string.h>int Len;struct TNode {int left , right;int col;TNode *LeftChild , *RightChild;void Construct ( int , int );void Insert ( int , int , int );void Calculate ();} Tree [16000] , *Root = &Tree [0];int CalColor [8001] , Many [8001];void TNode :: Construct ( int l , int r ){left = l; right = r;if ( l + 1 == r ) { LeftChild = NULL; RightChild = NULL; return; }int mid = ( l + r ) >> 1;LeftChild = &Tree [Len ++];RightChild = &Tree [Len ++];LeftChild->Construct( l , mid );RightChild->Construct( mid , r );}void TNode :: Insert ( int l , int r , int c ) {if ( col == c ) return;if ( l == left && r == right ) { col = c; return; } int mid = ( left + right ) >> 1;if ( col != MulColor ) { LeftChild -> col = col; RightChild -> col = col; }col = MulColor;if ( r <= mid ) { LeftChild -> Insert ( l , r , c ); return; }if ( l >= mid ) { RightChild -> Insert ( l , r , c ); return; }LeftChild -> Insert ( l , mid , c );RightChild -> Insert ( mid , r , c );}void TNode :: Calculate (){if ( col != MulColor && col != NoColor ) {int i;for ( i = left; i < right; i ++ ) CalColor [i] = col;}if ( col == MulColor ) { LeftChild -> Calculate (); RightChild -> Calculate (); }}main (){int Total , a , b , c , i , t;Len = 1; Tree [0].Construct( 0 , 8000 );// printf ( "After Construct the Tree , Len = %d\n" , Len );while ( scanf ( "%d" , &Total ) != EOF ) {Tree [0].col = NoColor;while ( Total ) {scanf ( "%d %d %d" , &a , &b , &c );Root -> Insert( a , b , c );Total --;}memset ( CalColor , 0xff , sizeof ( CalColor ) ); memset ( Many , 0 , sizeof ( Many ));Root -> Calculate ();t = -1;for ( i = 0; i <= 8000; i ++ ) {if ( CalColor [i] == t ) continue;t = CalColor [i];if ( t != -1 ) Many [t] ++;}for ( i = 0; i <= 8000; i ++ ) if ( Many [i] ) printf ( "%d %d\n" , i , Many [i] );printf ( "\n" );}}// Problem zju2451// DP with Segment Tree#include <stdio.h>#define MAX 50000int Len;struct TNode {int left , right;int minstep;TNode *LeftChild , *RightChild;void Construct ( int , int );void Insert ( int , int );int GetRank ( int , int );} STree [MAX * 2 + 2] , *Root = &STree [0];int N , M;void TNode :: Construct ( int l , int r ){left = l; right = r; minstep = 999999;if ( l == r ) { LeftChild = NULL; RightChild = NULL; return; }int mid = ( l + r ) >> 1;LeftChild = &STree [Len ++];RightChild = &STree [Len ++];LeftChild->Construct ( l , mid );RightChild->Construct( mid + 1 , r );}void TNode :: Insert ( int p , int x ){if ( x < minstep ) minstep = x;if ( left == right ) return;if ( p <= ( left + right ) >> 1 ) LeftChild->Insert( p , x );else RightChild->Insert( p , x );}int TNode :: GetRank ( int l , int r ){if ( l == left && r == right ) return minstep;int mid = ( left + right ) >> 1;if ( r <= mid ) return LeftChild->GetRank( l , r );if ( l > mid ) return RightChild->GetRank( l , r ); int ret1 , ret2;ret1 = LeftChild->GetRank( l , mid );ret2 = RightChild->GetRank( mid + 1 , r ); return ret1 < ret2 ? ret1 : ret2;}main (){int i , a , b , p;while ( scanf ( "%d %d" , &N , &M ) != EOF ) { Len = 1; Root->Construct( 1 , N );Root->Insert ( 1 , 0 );for ( i = 0; i < M; i ++ ) {scanf ( "%d%d" , &a , &b );if ( a < b ) {p = Root->GetRank ( a , b - 1 );Root->Insert ( b , p + 1 );}}printf ( "%d\n" , Root->GetRank( N , N ) ); }}// PKU 2104// Segment Tree && Binnary Search#include <stdio.h>#define MAX 100000int len;struct TNode {int left , right;char depth;TNode *LeftChild , *RightChild;void construct ( int , int , int );int GetRank ();} Node [2 * MAX + 2];int SortArray [18] [MAX + 2];int Key , ls , rs;void TNode :: construct ( int l , int r , int dep ) {left = l; right = r; depth = dep;if ( left == right ) {scanf ( "%d" , &SortArray [dep] [l] );return;}int mid = ( l + r ) >> 1;LeftChild = &Node [len ++];LeftChild->construct( l , mid , dep + 1 );RightChild = &Node [len ++];RightChild->construct( mid + 1 , right , dep + 1 );int i = left , j = mid + 1 , k = left;while ( i <= mid && j <= r ) {if ( SortArray [dep + 1] [i] < SortArray [dep + 1] [j] )SortArray [dep] [k ++] = SortArray [dep + 1] [i ++];elseSortArray [dep] [k ++] = SortArray [dep + 1] [j ++];}while ( i <= mid ) SortArray [dep] [k ++] = SortArray [dep + 1] [i ++];while ( j <= right ) SortArray [dep] [k ++] = SortArray [dep + 1] [j ++];}int TNode :: GetRank (){if ( ls <= left && right <= rs ) {if ( SortArray [depth] [left] >= Key ) return 0;if ( SortArray [depth] [right] < Key ) return right - left + 1;if ( SortArray [depth] [right] == Key ) return right - left;int low = left , high = right , mid;while ( low + 1 < high ) {mid = ( low + high ) >> 1;if ( SortArray [depth] [mid] < Key ) low = mid;else high = mid;}return low - left + 1;}int ret = 0;if ( ls <= LeftChild->right ) ret += LeftChild->GetRank();if ( RightChild->left <= rs ) ret += RightChild->GetRank();return ret;}main (){int N , Q , i;int low , high , mid , Index;scanf ( "%d%d" , &N , &Q );len = 1; Node [0].construct( 0 , N - 1 , 0 );for ( i = 0; i < Q; i ++ ) {scanf ( "%d%d%d" , &ls , &rs , &Index );ls --; rs --;low = 0; high = N;while ( low + 1 < high ) {mid = ( low + high ) >> 1;Key = SortArray [0] [mid];if ( Node [0].GetRank() >= Index ) high = mid; else low = mid;}printf ( "%d\n" , SortArray [0] [low] );}}。