线段树
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
将线段树扩展到高维: 二维及更高维的局部和、最值维护问题。 1.对划分方案进行调整: 一维:一分为二; 二维:一分为四;(左上、右上、左下、右下) 三维:一分为八 退化现象严重: 例如查询矩形(二维)的某一整行,四分树将查询到每个 1*1的矩形,再返回,复杂度O(n)。
动规: f(i,0),f(i,1)表示到第i条栅栏左右端点的最小距离, f(i,0)=min{ f(j,0)+|ai-aj|, f(j,1)+|bj-ai| } i<j<=n 且栅栏j能直接到栅栏i。 时间复杂度O(n2)。 栅栏j能否直接到栅栏i需按左右端点分情况处理。
换个思路,其实我们只关心能直接到当前端点的那个端点 的最小距离,至于它是左端点还是右端点无所谓。 如栅栏j的某个端点(xj,j)能直接到栅栏i的某个端点(xi,i), 考虑f(j)+|xi-xj| =f(j)-xj+xi (xj<xi) =f(j)+xj-xi (xi>xj) 对于端点(xj,j),不能只维护f(j),而是维护f(j)-xj和f(j)+xj两 个值,用来参与(xi,i)的计算。
一般的模拟算法的时间复杂度可以达到O(n^3),不理想。
用线段树来解决:
预处理,坐标离散化;
在x轴上维护一棵线段树,区间覆盖模型:
把所有矩形的上下边界按y升序排 把矩形的上下两边界看作对x轴的覆盖 下边界对应对x轴的覆盖; 上边界对应对x轴的删除覆盖。
对于每个y,
统计线段树中已经被覆盖的
总长度T.sum,
数据结构的规划: 设到端点(xj,j)的最小距离为k,维护(k-xj)、(k+xj)。 求到端点(xi,i)的最小距离, 对于xj<xi,取min{ k-xj+xi } 对于xj>xi,取min{ k+xj-xi } 到端点(xi,i)的最小距离等于上述两者再取min,记为k’ 维护(k’-xi)、(k’+xi)。 区间查询和单点修改,可以利用线段树。 最后一点,栅栏i[ai,bi]添加后,被栅栏i包含的端点就不能 直接更新i之后的栅栏了,区间(ai,bi)的值修改为无穷大。
查询区间和
int query(int k,int ll,int rr){
//查询[ll,rr)的区间和
if(ll<=t[k].Left&&rr>=t[k].Right) return t[k].sum; int ans=0; if(ll<(t[k].Left+t[k].Right)/2) ans+=query(t[k].lptr,ll,rr); if(rr>(t[k].Left+t[k].Right)/2) ans+=query(t[k].rptr,ll,rr);
T[a,(a+b)/2)、T[(a+b)/2,b)。
若b-a=1,T[a,b)是叶结点,单位区间。
线段树的性质
结点数:数列长度为n,总结点个数不超过2*n。 深度:近似满二叉树,深度不超过log2(n-1)+1。
线段树把区间上的任意一条长度为L的线段都分成了不超
过2*log2L条线段,查询在O(log2n)的时间内解决。
int lptr,rptr;
//区间[Left,right)
//左右孩子指针
};
线段树的存储
3.堆结构存储 线段树去除了最底层之后是一棵满二叉树,因此可以 用类似堆的存储方式。 根结点存储在1号位置,对于存储在i号位置的结点而
言,其左孩子存储在2*i号位置,右孩子存储在2*i+1号位
置。
最后一层可能很空,造成空间的浪费。
问题2.有一列长度为n的数,刚开始全是0。现在执行m
次操作,每次可以执行以下两种操作之一:
(1)将数列中某个区间的所有数加上某个数值; (2)询问给定区间中所有数的和。 若直接用前面的方法维护修改操作的话,对单个元素的 修改操作是O(log2n),那么区间修改是O(n*log2n),比直
接模拟还慢。
void update(int k){ //延迟信息下传
t[t[k].lptr].sum+=t[k].bj*(t[t[k].lptr].Right-t[t[k].lptr].Left);
t[t[k].rptr].sum+=t[k].bj*(t[t[k].rptr].Right-t[t[k].rptr].Left); t[t[k].lptr].bj+=t[k].bj; t[t[k].rptr].bj+=t[k].bj; t[k].bj=0;
输入格式: 样例输入: 输出: 第一行是两个整数n和s,其中s的绝 4 0 4 对值不超过100000。 -2 1 接下来的n行,每行两个整数ai和bi, -1 2 代表第i个栅栏的两个端点的横坐标。-3 0 其中 -2 1 100000<=ai<bi<=100000,an<=s<=bn。 输出格式: 输出仅一个整数,代表x轴方向所需 要走的最短距离。
[1,5)
[1,1 0) [5,1 0)
Βιβλιοθήκη Baidu
[1,3)
[1,2) [2,3)
[3,5)
[3,4) [4,5)
[5,7)
[5,6) [6,7)
[7,1 0) [7,8)
[8,1 0)
[9,1 0)
[8,9)
线段树的结构
区间:[a,b) 结点T[a,b)维护原数列中区间[a,b)的信息
若b-a>1,T[a,b)是分支结点,其左右孩子分别为:
线段树的结构
[1,5) 5
[1,1 0) 10
[5,1 0) 14
[1,3) 3
[1,2) 2 [2,3) 4
[3,5) 7
[3,4) 6 [4,5) 8
[5,7) 11
[5,6) 10 [6,7) 12
[7,1 0) 16 [8,1 [7,8) 0) 14 17 [9,1 [8,9) 0) 16
}
int query2(int k,int ll,int rr){ if(ll<=t[k].Left&&rr>=t[k].Right) return t[k].sum; if(t[k].bj) update(k); int ans=0; if(ll<(t[k].Left+t[k].Right)/2) ans+=query2(t[k].lptr,ll,rr); if(rr>(t[k].Left+t[k].Right)/2) ans+=query2(t[k].rptr,ll,rr); return ans; }
线段树的结构
[1,5) 5
[1,1 0) 10->9
[5,1 0) 14->13
[1,3) 3
[1,2) 2 [2,3) 4
[3,5) 7
[3,4) 6 [4,5) 8
[5,7) 11
[5,6) 10 [6,7) 12
[7,1 0) 16->15 [8,1 [7,8) 0) 14 17 [9,1 [8,9) 0) 16
本次扫描出来的面积=
t.sum*(cur.y-pre.y)
对于y坐标
相同的边
界,全部
处理完毕
后, 再计算面 积的增量。
栅栏 Farmer John为奶牛们设置了一个障碍赛。障碍赛中有n个 (n<=50000)种各种长度的栅栏,每个都与x轴平行,其中 第i个栅栏的y坐标为i。 终点在坐标原点(0,0),起点在(s,n)。 奶牛们很笨拙,它们都是绕过栅栏而不是跳过去的。也就 是说它们会沿着栅栏走直到栅栏头,然后向着x轴奔跑, 直到碰到下一个栅栏挡住去路,然后再绕过去…… 如果按照这种方法,奶牛从起点到终点需要走的最短距离 是多少?(平行于y轴的距离不用计算了,因为这个数是n)
(Left+Right)是奇数,存储在Left+Right-2位置。
线段树的结构
[1,5) 5
[1,1 0) 10
[5,1 0) 14
[1,3) 3
[1,2) 2 [2,3) 4
[3,5) 7
[3,4) 6 [4,5) 8
[5,7) 11
[5,6) 10 [6,7) 12
[7,1 0) 16 [8,1 [7,8) 0) 14 17 [9,1 [8,9) 0) 16
线段树的存储
4.更紧凑的存储方式 对于结点区间为[Left,Right]的结点,存储在 (Left+Right)|(Left!=Right)号位置, 把每个结点不重复且不遗漏地映射到[2,2n]中。
如果结点区间为[Left,Right)左闭右开区间形式,存储在
Left+Right-1位置,如果[Left,Right)不是单位区间且
return ans;
}
线段树的修改 void change1(int k,int p,int delta){ //修改单个元素的值 if(t[k].Left==t[k].Right-1) t[k].sum += delta; else{ if(p<( t[k].Left + t[k].Right) / 2 ) change1( t[k].lptr , p , delta ); if(p>=( t[k].Left + t[k].Right) / 2 ) change1( t[k].rptr , p , delta ); t[k].sum=t[t[k].lptr].sum + t[t[k].rptr].sum; } }
空间复杂度:O(n)。
线段树的存储
1.链表存储 struct linkst{
int Left,Right;
//区间[Left,right)
linkst *lptr,*rptr; //左右孩子指针
};
线段树的存储
2.数组模拟链表存储 struct arrst{ //数组模拟链表
int Left,Right;
线段树的延迟修改
在递归修改时,如果发现修改的区间已经覆盖了当前结
点,就停止递归,给当前结点作上标记。 在以后的维护或查询过程中,需要对这样的结点的标记 进行分解,传递给它的子结点。 复杂度O(log2n)。
void change2(int k,int ll,int rr,int delta){ if( ll <= t[k].Left && rr >= t[k].Right ){ t[k].sum += delta * ( t[k].Right - t[k].Left ); //更新sum t[k].bj += delta; //累计修改 }else{ if( t[k].bj ) update(k); //传递修改 if(ll < (t[k].Left +t[k].Right)/2) change2(t[k].lptr,ll,rr,delta); if(rr > (t[k].Left+t[k].Right)/2) change2(t[k].rptr,ll,rr,delta); t[k].sum=t[t[k].lptr].sum+t[t[k].rptr].sum; } }
Input Sample: 2 Input: 10 10 20 20 第一行:一个整数n(1<=n<=10000); Output Sample: 以下n行:每行4个整数 225 x1,y1,x2,y2(0<=x1<x2<=30000,0<=y1<y2<=30000)。
矩形覆盖问题 (x1,y1)和(x2,y2)表示矩形的左下角和右上角坐标 Output: 一个整数,表示所有矩形的公共面积。
线段树的构造 需要维护额外的信息来达到解决问题的目的。 问题1中,每个结点需维护对应区间的和。
struct node{
int Left,Right; //区间[Left,Right)
int lptr,rptr;
int sum; }t[2*maxn];
//左右孩子指针
//区间和
建树 //递归建立线段树,下标由1开始 void buildtree(int ll,int rr){ int cur=++tot; t[cur].Left=ll; t[cur].Right=rr; if(ll!=rr-1){ t[cur].lptr=tot+1; buildtree(ll,(ll+rr)/2); t[cur].rptr=tot+1; buildtree((ll+rr)/2,rr); t[cur].sum=t[t[cur].lptr].sum+t[t[cur].rptr].sum; }else t[cur].sum=a[ll]; }
线 段 树
问题1:有一列长度为n的数,刚开始全是0。现在执行m 次操作,每次可以执行以下两种操作之一: (1)将数列中的某个数加上某个数值; (2)询问给定区间中所有数的和。
朴素的模拟:
修改O(1)、查询O(n)、总复杂度O(n*m)。
低效的原因:维护是针对元素的,但查询是针对区间的。
线段树的结构