线段树(单标记+离散化+扫描线+双标记)+zkw线段树+权值线段树+主席树及一些例题

“队列进出图上的方向php


线段树区间修改求出总量

可持久留下的迹象

咱们 俯身欣赏” ----《膜你抄》
 

线段树很早就会写了,但一直没有总结,因此偶尔重写又会懵逼,因此还是要总结一下。html

 

引言node

在生活和竞赛中,咱们老是会赶上一些问题,好比说使人厌恶的统计成绩,老师会想询问几我的中成绩最低的是谁......ios

因而问题出现了。c++

 

e.g.1(暴力膜不可取)算法

已知班上有50个学生,学号分别为1-50,老师想问学号为a-b之间的最低分是多少数组

好比 2 5 3 4 1中 2-4 之间的最小值为 3数据结构

显然数据很是少,咱们能够针对每一个询问,扫一遍,得到最小值。函数

复杂度呢?假设有m个询问,a-b的区间最大为npost

因此复杂度为Θ(mn)

 

e.g.2(st表)

那么新问题来了,一场多市联考之后,你的老师拿到了一份有100000人成绩的表格(并无任何科学依据,纯属胡诌,若有雷同,不胜荣幸),老师如今想问你a-b之间的最低分是多少,并在一秒内出解。

说句实话老师你为何必定要在1秒内出解啊

反正就当老师赶时间吧(摊手)

由于是静态查询,咱们能够用st表来作,这就不详细说了。

复杂度为Θ(询问次数),但预处理是Θ(nlogn)的。

 代码以下:

#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; int n,m,dp[100010][17]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&dp[i][0]); } for(int i=1;i<=17;i++) { for(int j=1;j<=n;j++) { if(j+(1<<(i-1))-1<=n) { dp[j][i]=min(dp[j][i-1],dp[j+(1<<(i-1))][i-1]); } } } int l,r; for(int i=1;i<=m;i++) { scanf("%d%d",&l,&r); int x=(int)(log((double)(r-l+1))/log(2.0)); printf("%d\n",min(dp[l][x],dp[r-(1<<x)+1][x])); } }

 

 

e.g.3(线段树点修改)

成绩还在复查,有时候会偶尔发现有些同窗的成绩算错了,而后要更新,因而老师又会想要询问a~b之间的最低分,仍是100000个学生.......

说句实话老师你难道不能等到复查完再查分吗

这道题和e.g.2有什么区别吗?

这道题是强制在线的,由于一个同窗的成绩改变之后会致使整个区间的值改变。

这样st表就失效了。

咱们会发现st表在查询上很优,但构造的话就.......

那有没有什么好的办法呢?

虽然st表有点小问题,但它的思想能够借鉴——两个小区间中最小值较小的那个是这个大区间的最小值

那么不妨想想

若是一个数值修改了,st表的哪些部分须要改动呢?

若是这些改动的部分并很少,咱们能够只改这些部分,就不用重构st表了!

不用想天然是全部覆盖这个点的区间。

这些区间有多少呢?长度为一的一个,为二的两个,为四的四个.......加起来彷佛太多了!与其如此,我还不如重构st表呢!

好的,那么现在的问题就变成了,该怎么让这些区间变得少一些。

继续开始yy,咱们的一个点在一个二的幂次的长度上只出现一次,那么样这一个点要修改值的时候,咱们只用修改包含它长度为2,4,8.....的点就能够了。

那么这是什么呢?一棵二叉树。

 

 

好的吧 上面的彷佛是错的,由于若是有不是2的幂次,树就建不起来了。

 

那么索性逆其道而行,咱们靠二分长度来建树。

 

数值树:

 

 区间树:

那么此时建出来的树中若是要修改一个点的怎么办呢

很简单,修改它全部的影响的父节点就好了

嘛,固然不是每一个父节点都要改,仍是要按题意来的啊~

好比这个:

那么怎么查询呢?

好比说我要查询上图2-4的最小值

可是会发现2-4并无完整对应的区间,若是有的话天然是能直接出解

 

那怎么办呢?

咱们能够经过二分肯定长度的中间值,而后判断咱们的查询的l是否小于mid,若是是,那么这个节点的左子树中有一部分解会影响到总解,咱们须要继续搜这颗子树的子树,最终若是对于一个子树它的l-r被a-b所包含,那么就不用再搜了。

好比查询区间2-4

如图则是查询访问的次序。

那么它的复杂度是多少呢?

显然建树是Θ(nlogn)修改是Θ(logn)

那么查询呢?

能够想象,对于一个区间,他必定包含一个长度小于他的最长的2的幂次的长度,好比说长度为十的区间中必定包含一个长度为八的区间

这个区间若是恰好对应一个相应的节点,那么它就至关于用了一的费用,查了八个解

但比较尬的是有时候这个八是被错开的,但再不济也能分红两个四,至关于用了二的费用查了八个解

那么对于这个区间每次最多须要用二的费用除掉最大的二的幂次的解

咱们知道1+2+4+8+......+2^n=2^(n+1)-1

而在2^(n+1)-1的范围内全部的数字都能用最多n个2的幂次之和表示

因此复杂度为Θ(logn)

 

e.g.3.1线段树点修改如何实现?

彷佛能够理解线段树的思想了,但其实仍是一脸懵逼(其实我估计通常人看不懂……)

那么就详细的拆开来说讲吧。

 

建树(build)

好的,那么应该怎么建树呢?

咱们须要快速的查询二叉树的每一个点的父节点和子节点,同是内存还要尽量省,咱们能够对于一个编号为n的点将它的父节点记为[n/2](取整),左儿子记为2*n,右儿子记为2*n+1

而后根节点为1,表示1-n区间中所要求的值(按题意来定)

这样子不会有冲突,由于一个深度n的树最多有2^n-1个节点,而深度为n+1的标号最小的子树为2^n。

而后建树从叶子节点开始,逐渐传到父节点,因此咱们还须要一个函数(push_up),来计算两个子节点到父节点的转移。

 

固然实际建树是dfs的,反正理解一发就好了!

主要须要两个函数:push_up,build

一、push_up

二、build

 

点更新(update)

天然是先更新那个点,而后更新他的全部父节点。

update

 

查询(query)

按照以前的解释已经说的很清楚了

query

 

 

这样子点修改的线段树就基本写出来了

那么来一道例题

 

hdu1754 i hate it

有n个数,m个操做,操做分为两种种类:

一、Q l r 询问l-r之间最大值

二、U x v 将x位置的值换成当前值与v值中较大的一个

输入样例#1:
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
输出样例#1: 
5
6
5
9

代码:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define lson root<<1
#define rson root<<1|1

int tree[800010],n,m,a,b,x,v; char c; void push_up(int root) { tree[root]=max(tree[lson],tree[rson]); } void build(int l,int r,int root) { if(l==r) { scanf("%d",&tree[root]); return ; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void update(int l,int r,int x,int v,int root) { if(l==r) { tree[root]=max(tree[root],v); return; } int mid=(l+r)>>1; if(x<=mid) { update(l,mid,x,v,lson); } else { update(mid+1,r,x,v,rson); } push_up(root); } int query(int a,int b,int l,int r,int root) { int ans=0; if(l>=a&&b>=r) { return tree[root]; } int mid=(l+r)>>1; if(a<=mid) { ans=max(ans,query(a,b,l,mid,lson)); } if(mid<b) { ans=max(ans,query(a,b,mid+1,r,rson)); } return ans; } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(1,n,1); for(int i=1; i<=m; i++) { scanf("\n%c",&c); if(c=='Q') { scanf("%d%d",&a,&b); printf("%d\n",query(a,b,1,n,1)); } if(c=='U') { scanf("%d%d",&x,&v); update(1,n,x,v,1); } } } return 0; }

 

 

固然也能够用结构体来储存树的结构,好比区间的左和右端点,这样子能够少传递几个参数,不过实际上速度并不会快不少,甚至会慢……不过这有什么问题吗,这玩意可能会更加好写2333

代码:

#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define lson root<<1
#define rson root<<1|1
using namespace std; struct node { int l,r,m; } tr[800080]; int n,m; void push_up(int root) { tr[root].m=max(tr[lson].m,tr[rson].m); } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; scanf("%d",&tr[root].m); return ; } tr[root].l=l; tr[root].r=r; int mid=(l+r)>>1; build(lson,l,mid); build(rson,mid+1,r); push_up(root); } void update(int root,int pos,int val) { if(tr[root].l==pos&&tr[root].r==pos) { tr[root].m=max(tr[root].m,val); return ; } int mid=(tr[root].l+tr[root].r)>>1; if(pos<=mid) { update(lson,pos,val); } else { update(rson,pos,val); } push_up(root); } int query(int root,int l,int r) { if(l==tr[root].l&&r==tr[root].r) { return tr[root].m; } int mid=(tr[root].l+tr[root].r)>>1; if(l>mid) { return query(rson,l,r); } else { if(mid>=r) { return query(lson,l,r); } else { return max(query(lson,l,mid),query(rson,mid+1,r)); } } } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(1,1,n); while(m--) { char kd; scanf("\n%c",&kd); if(kd=='Q') { int l,r; scanf("%d%d",&l,&r); printf("%d\n",query(1,l,r)); } if(kd=='U') { int pos,val; scanf("%d%d",&pos,&val); update(1,pos,val); } } } }

 

 

上面那题为单点修改求区间最大值模板

那么咱们再来一道单点修改求区间和模板题

 

hdu1166

给出n个点以及一些操做,操做分为四种

一、Add x v 给x的位置加v

二、Sub x v 给x的位置减v

三、Query l r 求l-r的区间和

四、End 结束询问

Sample Input
1
10
1 2 3 4 5 6 7 8 9 10
Query 1 3
Add 3 6
Query 2 7
Sub 10 2
Add 6 3
Query 3 10
End 

Sample Output
Case 1:
6
33
59

代码:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define lson root<<1
#define rson root<<1|1

int tree[800010],n,m,a,b,x,v; char c[10]; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build(int l,int r,int root) { if(l==r) { scanf("%d",&tree[root]); return ; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void update(int l,int r,int x,int v,int root) { if(l==r) { tree[root]+=v; return; } int mid=(l+r)>>1; if(x<=mid) { update(l,mid,x,v,lson); } else { update(mid+1,r,x,v,rson); } push_up(root); } int query(int a,int b,int l,int r,int root) { int ans=0; if(l>=a&&b>=r) { return tree[root]; } int mid=(l+r)>>1; if(a<=mid) { ans+=query(a,b,l,mid,lson); } if(mid<b) { ans+=query(a,b,mid+1,r,rson); } return ans; } int main() { int t,ttt=0; scanf("%d",&t); while(t--) { ttt++; printf("Case %d:\n",ttt); memset(tree,0,sizeof(tree)); scanf("%d",&n); build(1,n,1); while(1) { cin>>c; if(c[0]=='Q') { scanf("%d%d",&a,&b); printf("%d\n",query(a,b,1,n,1)); } if(c[0]=='A') { scanf("%d%d",&x,&v); update(1,n,x,v,1); } if(c[0]=='S') { scanf("%d%d",&x,&v); update(1,n,x,-v,1); } if(c[0]=='E') { break; } } } return 0; }

 

天然也能够写结构体板的,但请容我偷个懒哈~(doge)

 

不不不不,偷懒实际上是为了写下面这道好题。

能够说若是下面这道题能写出来,就说明你对线段树的结构有必定的了解了。

 

洛谷U23283(原题为codeforces 914D,这款游戏就不要玩了,太伤身体

 

题目大意

给出一段序列,两个操做

操做1 给出l,r,x

求区间l-r的gcd,若是至多能改掉区间内的一个数(不影响原序列),使gcd是x的倍数,那么输出YES,不然输出NO

 

操做2 给出pos,x

将序列中pos位置上的数字改成x

 

首先GCD是具备传递性的,因此可使用线段树进行维护,但比较麻烦的就是能够改掉一个数。

能够试想一下,对于每一个区间的查询,咱们最终获得的是若干完整返回块的gcd,若是其中有两个块gcd都不是x的倍数,那么确定GG

若是只有一个块不是呢?这个块中也可能有多个数不是x的倍数,还须要再检验一下。

线段树的思路就是一个节点的数值由他的两个儿子节点转移而来,若是他的两个儿子节点的gcd都不是x的倍数,那么仍是GG

若是只有一个不是,咱们就继续查看那个点的左右儿子gcd是否都不是x的倍数,直到长度为一的节点便可,此时必定知足条件

每次能够去掉一半的长度,至关于二分。

算上gcd带的log的状况下,复杂度为Θ(n*logn*logn)

代码以下:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define lson root<<1
#define rson root<<1|1
using namespace std; int gcd(int a,int b) { if(b>a) { swap(a,b); } if(b) { return gcd(b,a%b); } else { return a; } } int nowson,x,cnt,n,m; struct node { int l,r,g; }tr[2000020]; void push(int root) { tr[root].g=gcd(tr[rson].g,tr[lson].g); } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; scanf("%d",&tr[root].g); return ; } int mid=(l+r)>>1; tr[root].l=l; tr[root].r=r; build(lson,l,mid); build(rson,mid+1,r); push(root); } void update(int root,int pos,int v) { if(tr[root].l==pos&&tr[root].r==pos) { tr[root].g=v; return ; } int mid=(tr[root].l+tr[root].r)>>1; if(mid>=pos) { update(lson,pos,v); } else { update(rson,pos,v); } push(root); } int query(int root,int l,int r) { if(tr[root].l==l&&tr[root].r==r) { if(tr[root].g%x!=0) { cnt--; nowson=root; } return tr[root].g; } int mid=(tr[root].l+tr[root].r)>>1; if(l>mid) { return query(rson,l,r); } else { if(r<=mid) { return query(lson,l,r); } else { return gcd(query(lson,l,mid),query(rson,mid+1,r)); } } } int check(int root) { if(tr[root].l==tr[root].r) { return 1; } if(tr[lson].g%x!=0&&tr[rson].g%x!=0) { return 0; } if(tr[lson].g%x==0) { return check(rson); } else { return check(lson); } } int main() { int n,m; scanf("%d",&n); build(1,1,n); scanf("%d",&m); for(int i=1;i<=m;i++) { int kd,l,r; scanf("%d",&kd); if(kd==1) { cnt=1; scanf("%d%d%d",&l,&r,&x); int tmp=query(1,l,r); if(x==tmp) { puts("YES"); } else { if((!cnt)&&check(nowson)) { puts("YES"); } else { puts("NO"); } } } else { scanf("%d%d",&l,&r); update(1,l,r); } } }

 

e.g.4(线段树区间修改)

复查时出现了评卷大错误,连续一个考场某道题的分都没有加,如今须要给加上,而后老师把聪明的你推荐给了统分人(做了吧233)要询问区间和以方便计算平均数。

好的吧,反正改一个也是改,改一段也是改。

 

来看一道例题 

 

洛谷p3372

如题,已知一个数列,你须要进行下面两种操做:

1.将某区间每个数加上x(区间加

2.求出某区间每个数的和(区间求和

 

最简单的思路天然是给每一个点都加上值而后push_up

代码:

#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define lson root<<1
#define rson root<<1|1 

long long tree[400040]; int n,m; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build(int l,int r,int root) { if(l==r) { scanf("%lld",&tree[root]); return; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void add(int l,int r,int root,int ls,int rs,int v) { if(l==r) { tree[root]+=v; return; } int mid=(l+r)>>1; if(ls<=mid) { add(l,mid,lson,ls,rs,v); } if(rs>mid) { add(mid+1,r,rson,ls,rs,v); } push_up(root); } long long query(int l,int r,int ls,int rs,int root) { if(l>=ls&&r<=rs) { return tree[root]; } int mid=(l+r)>>1; long long ans=0; if(ls<=mid) { ans+=query(l,mid,ls,rs,lson); } if(rs>mid) { ans+=query(mid+1,r,ls,rs,rson); } return ans; } int main() { int n,m; scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { int kd; int l,r,v; scanf("%d",&kd); if(kd==1) { scanf("%d%d%d",&l,&r,&v); add(1,n,1,l,r,v); } if(kd==2) { scanf("%d%d",&l,&r); int ans=query(1,n,l,r,1); printf("%lld\n",ans); } } } 

 

 

而后就光荣的TLE了,咱们考虑再优化一发

 

其实彻底不用每一个点push_up

由于咱们彻底能够用l-r区间中包含的须要修改的点的个数来算出该区间须要加上的值,即为修改点个数乘以每一个点修改的值。

这样子能够省掉很多时间,由于将加法变成乘法后本来复杂度为Θ(长度)的加就变成了Θ(1)的乘

代码:

#include<cstdio> #include<vector> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define lson root<<1
#define rson root<<1|1 

long long tree[400040]; int n,m; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build(int l,int r,int root) { if(l==r) { scanf("%lld",&tree[root]); return; } int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void add(int l,int r,int root,int ls,int rs,int v) { if(l==r) { tree[root]+=v; return; } int mid=(l+r)>>1; if(ls<=mid) { add(l,mid,lson,ls,rs,v); } if(rs>mid) { add(mid+1,r,rson,ls,rs,v); } push_up(root); } long long query(int l,int r,int ls,int rs,int root) { if(l>=ls&&r<=rs) { return tree[root]; } int mid=(l+r)>>1; long long ans=0; if(ls<=mid) { ans+=query(l,mid,ls,rs,lson); } if(rs>mid) { ans+=query(mid+1,r,ls,rs,rson); } return ans; } int main() { int n,m; scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { int kd; int l,r,v; scanf("%d",&kd); if(kd==1) { scanf("%d%d%d",&l,&r,&v); add(1,n,1,l,r,v); } if(kd==2) { scanf("%d%d",&l,&r); int ans=query(1,n,l,r,1); printf("%lld\n",ans); } } } 

 

 

可是由于每个修改的叶节点的父节点及祖先节点都须要遍历,复杂度彷佛还有点可怕(但比起以前快了近两倍)

因此又一次光荣的TLE了

可是此次计算中的乘思想是比较有启发的。

那该怎么继续优化呢?

ちょっとまって!咱们以前的查询为何是Θ(logN)的呢?

咱们能够发现,其实查询的时候咱们遵循能返回大块就返回大块的思路,不会再下去查询小块

而咱们却在每次更新时都会去将全部的大块和小块一并更新,

那么能不能只更新大块呢?

显然不行

由于咱们仍然可能会须要查询一些更小的区间的值。

可是咱们能够退而求其次,等到须要查找更小的区间的时候再去更新

因而就能够给每个点打上一个标记,等咱们访问了他的父节点再将他的父节点附上这个值,并将这个值向下推给他,这就是lazy-tag的思想。

须要一个新的函数:用于将懒惰标记下传。

push_down

固然原来的其余函数也要有所改动,具体看代码:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define lson root<<1
#define rson root<<1|1
using namespace std; struct node { long long l,r,sum,lazy; }tree[400040]; int n,m,kd; void push_up(int root) { tree[root].sum=tree[lson].sum+tree[rson].sum; } void push_down(int root) { int mid=(tree[root].l+tree[root].r)>>1; tree[lson].sum+=tree[root].lazy*(mid-tree[root].l+1); tree[rson].sum+=tree[root].lazy*(tree[root].r-mid); tree[lson].lazy+=tree[root].lazy; tree[rson].lazy+=tree[root].lazy; tree[root].lazy=0; } void build(int l,int r,int root) { if(l==r) { tree[root].l=l; tree[root].r=r; scanf("%lld",&tree[root].sum); return; } tree[root].l=l; tree[root].r=r; int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); } void add(int l,int r,int root,int x) { if(l==tree[root].l&&r==tree[root].r) { tree[root].lazy+=x; tree[root].sum+=x*(tree[root].r-tree[root].l+1); return; } int mid=(tree[root].l+tree[root].r)>>1; if(tree[root].lazy) { push_down(root); } if(r<=mid) { add(l,r,lson,x); } else { if(l>mid) { add(l,r,rson,x); } else { add(l,mid,lson,x); add(mid+1,r,rson,x); } } push_up(root); } long long query(int l,int r,int root) { if(l==tree[root].l&&tree[root].r==r) { return tree[root].sum; } int mid=(tree[root].l+tree[root].r)>>1; if(tree[root].lazy) { push_down(root); } if(r<=mid) { return query(l,r,lson); } else { if(l>mid) { return query(l,r,rson); } } return query(l,mid,lson)+query(mid+1,r,rson); } int main() { scanf("%d%d",&n,&m); build(1,n,1); for(int i=1;i<=m;i++) { scanf("%d",&kd); if(kd==1) { int l,r,x; scanf("%d%d%d",&l,&r,&x); add(l,r,1,x); } else { int l,r; scanf("%d%d",&l,&r); printf("%lld\n",query(l,r,1)); } } }

 

这就是lazy标记比较浅薄的应用,咱们能够再来看一道比较抽象的题目

 

POJ2528

题目大意:

在墙上按照输入顺序贴海报,求最后能看见几张不一样的海报

emmm,这道题要用离散化的思路,由于原来的区间实在是太大了!

这是也我选这道题的惟一缘由

至于倒贴海报什么的我却是不敢苟同,由于明显有更加暴力的方法啊……

来来来,让咱们直接染色2333

对于区间l[i]-r[i]进行染色,其实就是对该区间进行区间修改,将这个区间修改为i

到时候用桶的思路去记录整面墙上出现了几种颜色便可。

对了,为了防止某些左右端点都被遮住,只有中间露出来的小透明影响答案,能够再离散化以前把l+1,r-1也扔进去。

我并不知道这玩意到底正不正确,可是居然A掉了

代码以下,若是有大佬能叉掉还请在评论区指正,注意这道题的本质只是为了向你们介绍线段树在赶上极大区间而实际使用的区间却没有这么多的时候可使用离散化的思想。

离散化听着高大上但其实代码也就下面这么一点,不要慌张哦~

可是必定要学会啊,以后权值线段树和主席树都是要用的啊

#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define lson root<<1
#define rson root<<1|1
using namespace std; struct node { int l,r,val,lazy; } tr[200020]; struct poster { int l,r; }p[40040]; int a[40040],cnt,ans[40040],cnt1[40040]; void push_down(int root) { tr[lson].val=tr[root].lazy; tr[rson].val=tr[root].lazy; tr[lson].lazy=tr[root].lazy; tr[rson].lazy=tr[root].lazy; tr[root].lazy=0; } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; tr[root].val=0; return ; } tr[root].l=l; tr[root].r=r; int mid=(tr[root].l+tr[root].r)>>1; build(lson,l,mid); build(rson,mid+1,r); } void update(int root,int l,int r,int val) { if(l==tr[root].l&&r==tr[root].r) { tr[root].val=val; tr[root].lazy=val; return ; } if(tr[root].lazy) { push_down(root); } int mid=(tr[root].l+tr[root].r)>>1; if(l>mid) { update(rson,l,r,val); } else { if(r<=mid) { update(lson,l,r,val); } else { update(lson,l,mid,val); update(rson,mid+1,r,val); } } } int query(int root,int pos) { if(pos==tr[root].l&&pos==tr[root].r) { return tr[root].val; } if(tr[root].lazy) { push_down(root); } int mid=(tr[root].l+tr[root].r)>>1; if(mid>=pos) { return query(lson,pos); } else { return query(rson,pos); } } int main() { int t,n; scanf("%d",&t); while(t--) { cnt=0; memset(cnt1,0,sizeof(cnt1)); scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d %d",&p[i].l,&p[i].r); a[++cnt]=p[i].l; a[++cnt]=p[i].l+1; a[++cnt]=p[i].r; a[++cnt]=p[i].r-1; } sort(a+1,a+cnt+1); //由此开始离散化 cnt=unique(a+1,a+cnt+1)-a-1; for(int i=1;i<=n;i++) { p[i].l=lower_bound(a+1,a+cnt+1,p[i].l)-a; p[i].r=lower_bound(a+1,a+cnt+1,p[i].r)-a; //到这结束 } build(1,1,cnt); for(int i=1;i<=n;i++) { update(1,p[i].l,p[i].r,i); } for(int i=1;i<=cnt;i++) { ans[i]=query(1,i); } int num=0; for(int i=1;i<=cnt;i++) { if(cnt1[ans[i]]==0&&ans[i]!=0) { cnt1[ans[i]]=1; num++; } } printf("%d\n",num); } }

 

 既然连离散化都讲了,那不妨也来说讲扫描线吧(什么神逻辑)

 

e.g.乱入 扫描线

 

 扫描线这玩意一听就很是高级啊

它具体使用的范围是计算几何里,求矩形的面积并

来看一道例题吧

POJ1151

题意:给出平面上n个矩形的左下角坐标和右上角坐标,求平面被全部矩形覆盖的面积。

好吧,这玩意跟线段树有什么关系?

我感受明显能够瞎搞啊

你瞧n<=200啊

可是若是可啪的出题人把数据范围形成了n<=100000呢(x,y的范围也极大)?

瞎搞不行了,咱们考虑一下哪里能优化

 

一个矩形的面积能够表示为长乘宽,那么一个矩形能作出的贡献就是从第一条边出现一直到第二条边出现,在这中间与y轴平行的线段的长度是不变的

有点蒙,那么来张图吧

因此想到了什么?

差分啊,差分啊差分啊

即便是几个矩形叠来叠去,在一条边出现到下一条边出现之间,与y轴平行的线段的长度也仍是不变的

 

因此对于y轴上的长度,只有在遇到新的线段的时候才会变化,咱们能够按照每条与y轴平行的边来分割矩形。

面积就是与y轴平行的当前长度和*(x[i]-x[i-1])

好的,那跟线段树有什么关系呢?

由于与y轴平行的长度和能够用线段树维护啊!

在第一条线段进入的时候对于y轴平行的那条线加入一条长度为ly[i]~ry[i]的线段

第二条线进入的时候删除这条线段,线段树维护长度和 ,这样子能够很是美妙的适应强制在线的很是大的数据

若是理解了的话,这就变成了一道区间覆盖问题,支持插入一条线段,删除一条线段,求当前全部线段覆盖的长度

此时的线段树要稍微更改一下

从push_up到update都要改(烦躁ing)

 

push_up的改变应该很好看懂,若是这一段lazy大于0,说明区间被全覆盖,长度为区间长度,不然则是两个子树的长度之和

 

build建右子树的时候mid+1改为mid,建的树同时记录实数的nl-nr和伪离散化的l-r

 

update见代码,不是很好解释,反正不难。

 

当初学的时候大概膜了好几份代码,这份是最好看懂得,我把它魔改了一发,应该也比较接近平日里本身的写法。

代码以下:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define lson root<<1
#define rson root<<1|1
#define N 100010
using namespace std; struct line { double x,y1,y2; int val; } l[N]; bool cmp(line a,line b) { return a.x<b.x; } struct node { double nl,nr,len; int l,r,lazy; } tr[N<<2]; double a[N]; void push_up(int root) { if(tr[root].lazy>0) { tr[root].len=tr[root].nr-tr[root].nl; } else { if(tr[root].r-tr[root].l==1) { tr[root].len=0; } else { tr[root].len=tr[lson].len+tr[rson].len; } } } void build(int root,int l,int r) { if(l+1==r) { tr[root].l=l,tr[root].r=r; tr[root].nl=a[l],tr[root].nr=a[r]; tr[root].lazy=0,tr[root].len=0.0; return ; } tr[root].l=l,tr[root].r=r; tr[root].nl=a[l],tr[root].nr=a[r]; tr[root].lazy=0,tr[root].len=0.0; int mid=(l+r)>>1; build(lson,l,mid); build(rson,mid,r); } void update(int root,double l,double r,int val) { if(tr[root].nl==l&&tr[root].nr==r) { tr[root].lazy+=val; push_up(root); return ; } if(r<=tr[lson].nr) { update(lson,l,r,val); } else { if(l>=tr[rson].nl) { update(rson,l,r,val); } else { update(lson,l,tr[lson].nr,val); update(rson,tr[rson].nl,r,val); } } push_up(root); } int main() { int n,cnt,ttt=0; while(scanf("%d",&n)!=EOF&&n) { ttt++; cnt=0; double x1,x2,y1,y2; for(int i=1;i<=n;i++) { scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2); l[++cnt].x=x1;l[cnt].y1=y1;l[cnt].y2=y2;l[cnt].val=1; a[cnt]=y1; l[++cnt].x=x2;l[cnt].y1=y1;l[cnt].y2=y2;l[cnt].val=-1; a[cnt]=y2; } sort(l+1,l+cnt+1,cmp); sort(a+1,a+cnt+1); build(1,1,cnt); update(1,l[1].y1,l[1].y2,l[1].val); double ans=0.0; for(int i=2;i<=cnt;i++) { ans+=tr[1].len*(l[i].x-l[i-1].x); update(1,l[i].y1,l[i].y2,l[i].val); } printf("Test case #%d\n",ttt); printf("Total explored area: %.6lf\n",ans); puts(""); } }

 

大致单标记的操做都有一些介绍了,更难的东西也是有的,但无外乎就是这个思想。

固然,除了单标记,还有双标记这种更加神奇的操做

 

e.g.5(线段树区间修改——双标记)

 啊,你终于帮助老师写出了一颗带区间修改的线段树,结果老师眉头一皱,发现事情并不简单。

原来,这些考场考得是不同的卷子,按照卷子的不一样要乘上不一样比例的权值。

简单地说,就是既要实现区间加又要实现区间乘,最后可怜的你仍是要出求区间和2333

这个例题就足以引出双标记了,其实若是理解了单标记,双标记也就不是很难了

双标记之因此有别于单标记,就是由于标记还有前后顺序。

 

前后顺序,这彷佛表明着一堆zz的分类讨论,可是不要慌,其实它并不复杂。

咱们来考虑一下哪一种计算先比较优越,

首先,是咱们的一号选手,人类最古老的数学符号,数学殿堂中的长者——加♂法

算了,太中二了,反正咱们就随便考虑一下先加后乘会咋样吧,好比说如今原数为a,加标记为b,乘标记为c

来来来,如今的值是(a+b)*c,好像没有什么问题呢~

呵呵,谁告诉你咱们实际修改的时候必定是加标记在前的呢?

若是先乘再加呢?

(a+(b/c))*c

emmm,看,个人精度,他飞起来啦qwq

 

好的,那么先乘再加就必定有用了吗?

sorry,先乘再加是真的能够随心所欲的!

为何呢?

由于乘标记再先乘后加是这样子的

a*c+b

先加后乘是这样子的

a*c+b*c

有没有发现,只须要在进行乘操做的时候把加标记乘上c就能够了。

反过来,加法也能够这么作,可是当心你的精度2333

 

仍是照例来到例题

洛谷P3373

已知一个数列,你须要进行下面三种操做:

1.将某区间每个数乘上x

2.将某区间每个数加上x

3.求出某区间每个数的和

代码写的时候注意开longlong 感谢hpw大佬的代码,对我颇有启发

代码以下:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define ll long long
#define lson root<<1
#define rson root<<1|1
#define N 400010
using namespace std; struct node { ll l,r,sum,add=0,mul=1; }tree[N]; int n,m,p; void push_up(int root) { tree[root].sum=(tree[lson].sum+tree[rson].sum)%p; } void push_down(int root) { int mid=(tree[root].l+tree[root].r)>>1; tree[lson].sum=(tree[lson].sum*tree[root].mul+tree[root].add*(mid-tree[root].l+1))%p; tree[rson].sum=(tree[rson].sum*tree[root].mul+tree[root].add*(tree[root].r-mid))%p; tree[lson].mul=(tree[lson].mul*tree[root].mul)%p; tree[rson].mul=(tree[rson].mul*tree[root].mul)%p; tree[lson].add=(tree[lson].add*tree[root].mul+tree[root].add)%p; tree[rson].add=(tree[rson].add*tree[root].mul+tree[root].add)%p; tree[root].add=0; tree[root].mul=1; return; } void build(int l,int r,int root) { if(l==r) { tree[root].l=l; tree[root].r=r; scanf("%lld",&tree[root].sum); return; } tree[root].l=l; tree[root].r=r; int mid=(l+r)>>1; build(l,mid,lson); build(mid+1,r,rson); push_up(root); return; } void add(int l,int r,int root,ll k) { if(l==tree[root].l&&r==tree[root].r) { tree[root].add=(tree[root].add+k)%p; tree[root].sum=(tree[root].sum+k*(tree[root].r-tree[root].l+1))%p; return ; } int mid=(tree[root].l+tree[root].r)>>1; push_down(root); if(r<=mid) { add(l,r,lson,k); } else { if(l>mid) { add(l,r,rson,k); } else { add(l,mid,lson,k); add(mid+1,r,rson,k); } } push_up(root); } void mul(int l,int r,int root,ll k) { if(l==tree[root].l&&r==tree[root].r) { tree[root].sum=(tree[root].sum*k)%p; tree[root].mul=(tree[root].mul*k)%p; tree[root].add=(tree[root].add*k)%p; return ; } int mid=(tree[root].l+tree[root].r)>>1; push_down(root); if (r<=mid) { mul(l,r,lson,k); } else { if (l>mid) { mul(l,r,rson,k); } else { mul (l,mid,lson,k); mul(mid+1,r,rson,k); } } push_up(root); } ll query (int l,int r,int root) { if(l==tree[root].l&&r==tree[root].r) { return tree[root].sum%p; } int mid=(tree[root].l+tree[root].r)>>1; push_down(root); if(r<=mid) { return query(l,r,lson); } else { if(l>mid) { return query(l,r,rson); } else { return (query(l,mid,lson)+query(mid+1,r,rson))%p; } } } int main () { scanf("%d%d%d",&n,&m,&p); build(1,n,1); for(int i=1; i<=m; ++i) { int oper; scanf("%d",&oper); if(oper==1) { int l,r; ll k; scanf("%d%d%lld",&l,&r,&k); mul(l,r,1,k); } else { if(oper==2) { int l,r; ll k; scanf("%d%d%lld",&l,&r,&k); add(l,r,1,k); } else { int l,r; scanf("%d%d",&l,&r); printf("%lld\n",query(l,r,1)%p); } } } return 0; }

 

 

e.g.6(zkw线段树点修改)

好了如今毒瘤的老师(出题人)已经不知足于100000的数据了,他魔改了时限和数据范围,几乎卡到了nlogn的极限,而后仍是询问区间最大值。

你以为你的线段树常数优越,随手一交就又一次完成了任务,可是你的菜鸡同窗xhk TLE了话说这不是给个人任务吗?为何xhk会来掺和一脚啊,好气啊!)

秉着同学的情谊,你开始教他zkw线段树

 

zkw线段树又被称为非递归版线段树,常数比普通线段树要小不少,这是由于zkw线段树用的几乎都是for循环,这省去了不少时间和空间。

由于c++的递归用的是栈机制,额外的空间复杂度与递归次数呈线性比例,同时,由于调用了大量函数,时间开销也会变大。

之因此须要普通线段树要用递归写,是由于递归好理解。

但其实非递归的线段树也不是很是难理解,很是难写。相反,它很短很精悍。

因此zkw神犇提出了它,并说明了它的几个优势:

常数小,空间小,代码短

那简直是太优了!因此zkw线段树应该怎么实现呢?

让咱们从新来看一看线段树的构造吧!

按照咱们存图的编号来讲它会是这样的:

 

由于咱们不用递归,因此建树时须要用一个科学的方法找到叶子节点,那么这种树合适吗?显然是不合适的,由于叶子结点没有连续性。上面可能还看不出来,这个就很明显了。

其中点1-6分别对应八、九、五、十二、1三、7

彻底没有任何简单规律。

因此树的结构要改一下,改为什么呢?

还记得最初那颗不科学的线段树吗?

这棵树的叶子结点是连续的 ,可是它只能支持2的幂次的建树。

为何呢?由于底层的叶子节点数不够多(一本正经的胡说八道)

那么就给它足够多的叶子结点啊(何不食肉糜式的回答233)

可是这真不难,你只须要先建一颗足够大的满二叉树,而后把那一个数组挂上去就好了。

有多大呢?天然是底层的满叶子结点个数大于数组的大小。

即满叶子结点个数为第一个大于n+1的2的幂次。

而后再把n所有挂上去。

是的,这看起来是比普通线段树耗内存,但其实也没超过四倍的内存,仍是能够接受的(顺便一提,他的空间复杂度仍是比通常线段树优越)。

并且,咱们能够直接得到这些要读入的叶子结点的位置。

接着咱们倒着一层一层往上推(由于一个点的编号一定大于它的父节点,咱们能够直接for i=bit;i>0;i--)

不过要注意,咱们不挂第一个叶子节点(为何先本身想一想)
因而建树就写出来了!

 

那么点修改呢?

是的,由于知道了叶子结点的位置,咱们能够很轻松地直接修改叶子结点,而后从下往上推父节点,这代码,清真!

可是查询略微有一点难理解

先举个栗子吧,若是我如今建了这么一棵有6个节点的zkw线段树,我要查询1-6的最小值,那么我须要的是哪几块?

没错,是图中的9,5,6,14

诶,那么每次先把l和r扔到底层而后一层一层往上跳,若是l是奇数就统计答案,r是偶数就统计答案,岂不美哉?

naive!

 来来来,再看看上面这张图

11的上面是5,,恭喜你,顺便把2也算进去了

因此咱们如今须要一种查询

 可以完成logn的查询区间最大值,并且不会错误的将不属于该区间的块记录进来

感受很难搞吧,其实还真有一种能够胜任此工做的查询方法,原理和上面的差很少

咱们在底层将l指向l-1,r指向r+1,像这张图同样跳上来

 

你发现了什么?

什么也没发现????

好吧,是在下输了,我再来张图,如今是查询3-5

 

这彷佛是很是明显了,若是l是偶数,就加上他的右兄弟(父亲节点的右子树),若是r是奇数,就加上他的左兄弟(父亲节点的左子树)

结束的条件是l==r-1

这能够保证查询到的区间必定是咱们想要的,由于这些区间的范围确定在(l-1,r+1)以内,被l和r限制着,STO zkw神犇

好的,因此能够很是轻松地发现,这种查询查询的是开区间。

这也就是咱们以前为何不挂地一个叶子节点的缘由。

为了方便查询开区间(0,n+1)的值,很显然咱们不会赶上n是上面15节点之类的状况,具体为何请回顾建树的过程

好的,查询代码以下:

 

如今zkw线段树全部须要用的代码都写完了,来个总代码:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define lson root<<1
#define rson root<<1|1

int n,m,bit; char c; int tree[800080]; void push_up(int root) { tree[root]=max(tree[lson],tree[rson]); } void build() { for(bit=1; bit<=(n+1); bit<<=1); for(int root=bit+1; root<=bit+n; root++) { scanf("%d",&tree[root]); } for(int root=bit-1; root>=1; root--) { push_up(root); } } void update(int root,int v) { for(tree[root+=bit]=v,root>>=1; root>0; root>>=1) { push_up(root); } } int query(int l,int r) { int ans=0; for(l+=bit-1,r+=bit+1; l^r^1; l>>=1,r>>=1) { if(~l&1) { ans=max(ans,tree[l^1]); } if(r&1) { ans=max(ans,tree[r^1]); } } return ans; } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(); for(int i=1; i<=m; i++) { int a,b; scanf("\n%c%d%d",&c,&a,&b); if(c=='Q') { printf("%d\n",query(a,b)); } else { update(a,b); } } } }

zkw线段树单点修改区间求和也是好写的, 以hdu1166为例

代码以下:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; #define lson root<<1
#define rson root<<1|1

int n,m,bit; char c[10]; int tree[800080]; void push_up(int root) { tree[root]=tree[lson]+tree[rson]; } void build() { for(bit=1; bit<=(n+1); bit<<=1); for(int root=bit+1; root<=bit+n; root++) { scanf("%d",&tree[root]); } for(int root=bit-1; root>=1; root--) { push_up(root); } } void update(int root,int v) { for(tree[root+=bit]+=v,root>>=1; root>0; root>>=1) { push_up(root); } } int query(int l,int r) { int ans=0; for(l+=bit-1,r+=bit+1; l^r^1; l>>=1,r>>=1) { if(~l&1) { ans+=tree[l^1]; } if(r&1) { ans+=tree[r^1]; } } return ans; } int main() { int t,ttt=0; scanf("%d",&t); while(t--) { ttt++; printf("Case %d:\n",ttt); memset(tree,0,sizeof(tree)); scanf("%d",&n); build(); for(; 1; ) { int a,b; scanf("\n%s",&c); if(c[0]=='E') { break; } scanf("%d %d",&a,&b); if(c[0]=='Q') { printf("%d\n",query(a,b)); } else { if(c[0]=='A') { update(a,b); } else { if(c[0]=='S') { update(a,-b); } } } } } }

至于区间修改,你会发现由于zkw是从下到上查询的因此标记什么的就gg了,可是zkw神犇提出了差分的作法,说句实话感受不明觉厉,因此区间修改仍是乖乖地写普通线段树吧~(之因此再也不写下去的缘由是由于没(zuo)有(zhe)必(hen)要(lan)了)

自带大常数的xhk你仍是爆零吧~

 至于zkw线段树有多优越,看看下面这两张图就知道了~感谢xhk提供的大常数普通线段树~

 

 

提及来普通线段树,老师灵机一动又改了需求

 

e.g.7(权值线段树求第k小,rank,前驱后继)

嗯,老师终于不来找你改分数了,他来询问你一个新的毒瘤问题

他会有六种操做

第一种:给你一个同窗的成绩

第二种:让你删掉一个同窗的成绩

第三种:询问这些成绩中第k小的是哪一个

第四种:询问某同窗的得分x在这些成绩中排第几

第五种:找到比该同窗高的最低分数

第六种:找到比该同窗低的最高分数

 

总操做数为100000

 

你嘿嘿一笑:“来,老师,splay、treap拿去不谢,不满意我这还有红黑树。”

老师眉头一皱:“我以为你以前那个算法挺好的,你就用那个来实现一下吧。”

好的,你开始写权值线段树。

 

若是咱们再也不按照区间建树而是改去按照每一个数出现的次数建树会怎么样?

好比说1,1,2,4,5这个序列,线段树建成这个样子,维护的是区间和

 

那么这有什么用呢?

 

咱们来分析一下这几个问题的具体意思

第一个第二个是插入和删除,就不须要分析了,pass,下一个!

第三个是查询第k小数

若是某个数以前(包括本身)有k个数以上,且这个数以前(不包括本身)的数的个数不到k个,那么这个数就是第k小

 

第四个是求某数的排名

这就是这个数以前数的个数加一

 

第五第六个前驱和后继就是这个数以前(以后)第一个出现的数,也就是个数大于等于一的最近的一个数,能够二分区间啦~

 

因此咱们只要有一个可以logn计算有多少个数比他小的数据结构就能够啦,是什么呢?

权值线段树

在上面那张图里,你能够经过query 1~x来求出x以前有多少个数啦~

 

因此这些问题就都迎刃而解了,能够实现每一个操做log^2 n之内。

 

如今来道例题

洛谷P3369

 

您须要写一种数据结构(可参考题目标题),来维护一些数,其中须要提供如下操做:

  1. 插入xx 数
  2. 删除xx 数(如有多个相同的数,因只删除一个)
  3. 查询xx 数的排名(排名定义为比当前数小的数的个数+1+1 。如有多个相同的数,因输出最小的排名)
  4. 查询排名为xx 的数
  5. xx 的前驱(前驱定义为小于xx ,且最大的数)
  6. xx 的后继(后继定义为大于xx ,且最小的数)

额,看着很简单?来,把数据范围供上来!

emmm,n仍是能够接受的,可是值域……太大了啊!

怕什么?咱们有离散化

kth和排名均可以logn搞,前驱后继我瞎胡了一种log^2 n的作法,若是有大佬可以给出logn的作法还请不吝赐教~

代码以下:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define lson root<<1
#define rson root<<1|1
using namespace std; struct node { int l,r,sum; } tr[800080]; struct opt { int kd,x; } op[100010]; int cnt,a[100010]; void push_up(int root) { tr[root].sum=tr[lson].sum+tr[rson].sum; } void build(int root,int l,int r) { if(l==r) { tr[root].l=l; tr[root].r=r; tr[root].sum=0; return ; } tr[root].l=l; tr[root].r=r; int mid=(l+r)>>1; build(lson,l,mid); build(rson,mid+1,r); push_up(root); } void update(int root,int pos,int val) { if(tr[root].l==pos&&tr[root].r==pos) { tr[root].sum+=val; return ; } int mid=(tr[root].l+tr[root].r)>>1; if(mid>=pos) { update(lson,pos,val); } else { update(rson,pos,val); } push_up(root); } int query(int root,int l,int r) { if(l>r) { return 0; } if(tr[root].l==l&&tr[root].r==r) { return tr[root].sum; } int mid=(tr[root].l+tr[root].r)>>1; if(mid<l) { return query(rson,l,r); } else { if(r<=mid) { return query(lson,l,r); } else { return query(lson,l,mid)+query(rson,mid+1,r); } } } int kth(int root,int k) { if(tr[root].l==tr[root].r) { return tr[root].l; } if(tr[lson].sum>=k) { return kth(lson,k); } else { return kth(rson,k-tr[lson].sum); } } int rank(int x) { int pos=lower_bound(a+1,a+cnt+1,x)-a; return query(1,1,pos-1)+1; } int pre(int root,int pos) { int l=1,r=pos-1,mid; while(l<r) { mid=(l+r)>>1; if(query(1,mid+1,r)) { l=mid+1; } else { r=mid; } } return r; } int next(int root,int pos) { int l=pos+1,r=cnt,mid; while(l<r) { mid=(l+r)>>1; if(query(1,l,mid)) { r=mid; } else { l=mid+1; } } return l; } int main() { int n; scanf("%d",&n); for(int i=1; i<=n; i++) { scanf("%d%d",&op[i].kd,&op[i].x); if(op[i].kd!=2&&op[i].kd!=4) { a[++cnt]=op[i].x; } } sort(a+1,a+cnt+1); cnt=unique(a+1,a+cnt+1)-a-1; build(1,1,cnt); for(int i=1; i<=n; i++) { if(op[i].kd==1) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; update(1,pos,1); } if(op[i].kd==2) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; update(1,pos,-1); } if(op[i].kd==3) { printf("%d\n",rank(op[i].x)); } if(op[i].kd==4) { int pos=kth(1,op[i].x); printf("%d\n",a[pos]); } if(op[i].kd==5) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; printf("%d\n",a[pre(1,pos)]); } if(op[i].kd==6) { int pos=lower_bound(a+1,a+cnt+1,op[i].x)-a; printf("%d\n",a[next(1,pos)]); } } }

 

好的,终于到主席树了(松气)

 

e.g.8(主席树求区间第k小)

毒瘤老师又一次提出了非分的要求,他但愿查找全部考试成绩中大家班的倒数第k名,即一段连续区间的第k小(为何一个班在同一个考场考呢,这是一个值得深思的问题)

 

区间第k小啊,这不是很好办呢。

反正求第k小的话,权值线段树的思路是能够借鉴的,而后咱们想一想若是对于区间l~r的第k大,咱们已经知道了1~l-1的权值线段树,又知道了1~r的权值线段树,那么根据前缀和的思路,咱们能够很轻松的求出l~r的权值线段树,对于这棵树进行求区间第k大便可

 

可是首先建n棵线段树就已经不是咱们可以接受的了

复杂度实在过高,确定不能适应100000的数据范围,同时,空间也是硬伤

 

那么怎么办呢?

咱们分析一下,每次主席树的操做,是否是全部的点都会变呢?

若是不会的话咱们天然能够有多少个变就改多少个了。

很幸运,与update的复杂度同样,每次插入新的数字只会改变logn个节点,那么咱们新建这logn个节点就能够了。(这玩意应该不用解释为何吧,由于update就只会遍历logn次)

 

对于每一个节点记录他的左子树右子树编号,这个编号再也不由root<<1,root<<1|1推得,而是由数组l和r记录

若是哪一个子树再也不被影响,那么就把他的原来的子树接上来。

主席树就是这个思路啦

查询的时候就查插入第l-1个数到插入第r个数的差的树的第k小就能够了

具体的操做就看看代码吧

 

照例来到例题:

洛谷P3834

题意就是求区间第k小

那么就是最裸的主席树。

代码以下:

#include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
#define mid ((l+r)>>1)
#define hi puts("hi");
using namespace std; #define N 200010

int n,m,q,cnt=0; int a[N],b[N],T[N]; int sum[N<<5],L[N<<5],R[N<<5]; int build(int l,int r) { int rt=++cnt; sum[rt]=0; if(l<r) { L[rt]=build(l,mid); R[rt]=build(mid+1,r); } return rt; } int update(int pre,int l,int r,int x) { int rt=++cnt; L[rt]=L[pre]; R[rt]=R[pre]; sum[rt]=sum[pre]+1; if(l<r) { if(x<=mid) { L[rt]=update(L[pre],l,mid,x); } else { R[rt]=update(R[pre],mid+1,r,x); } } return rt; } int query(int u,int v,int l,int r,int k) { if(l>=r) { return l; } int x=sum[L[v]]-sum[L[u]]; if(x>=k) { return query(L[u],L[v],l,mid,k); } else { return query(R[u],R[v],mid+1,r,k-x); } } int main() { scanf("%d%d",&n,&q); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); b[i]=a[i]; } sort(b+1,b+n+1); m=unique(b+1,b+1+n)-b-1; T[0]=build(1,m); for(int i=1;i<=n;i++) { int t=lower_bound(b+1,b+1+m,a[i])-b; T[i]=update(T[i-1],1,m,t); } while(q--) { int ll,rr,kk; scanf("%d%d%d",&ll,&rr,&kk); int t=query(T[ll-1],T[rr],1,m,kk); printf("%d\n",b[t]); } return 0; }

 

 

 

啊,写了三天终于写完了,原本当初的flag还有二维线段树和树状数组的,结果是在写不动了orz

平均一天3000字(不含制图)真的快要遇上网文做家了,代码写到死啊,不过仍是颇有成就感的

嗯,就这样了

相关文章
相关标签/搜索