ROS感到十分的惭愧因为我居然来解析这个我看了好久好久的普及组算法并且感到了蒟蒻的身份之沉重
说实话因为ROS也是最近才算完全学明白线段树(最近才盲敲整"棵"线段树),所以硬要说解析的话可能做不到。
ROS是通过洛谷P3372学会线段树的(本身就是个板子)
(ROS是看这篇洛谷题解学会线段树的)
在此先把代码贴这,如果要学习线段树的话强烈建议去看上面那篇题解去!讲的很系统很全面!
虽然我一开始看的时候有很多东西没看懂
所以ROS在此还是简单讲解一下线段树吧
(我在此只是讲一下我遇到的难点以及我所学习的线段树中的各个函数的意义)
如果写的想我上次那篇KMP的解析一样详细的话我就累死了
首先,什么是线段树?
上百度百科:
只给定义的话可能你无法理解线段树的好,所以便需要情景了!
(情景见我在上处发的题)
在这个情境中,如果我们没有学过线段树的话可能第一感觉是直接开数组然后直接用最暴力的方法求解。emmm看下数据范围,估计这个方法能拿30分吧。
但是毕竟NOIP不可能考一个线段树的板子
准确来说,在更多时候线段树只是我们在做题的一个工具。所以照某些OIer的话说,我们应该“把线段树刻进DNA中”。到最后应该做到盲敲线段树的水平
(然鹅ROS还做不到)
所以可能很多OIer的第二想法是,分块!
(ROS还真的把分块的代码写了一下看看能得多少分)
写完了结果发现........
这道题居然可以用分块AC(本来以为第二个范围是给分块的分数结果AC了就尴尬)
口说无凭先上图:
代码:
1 #include<bits/stdc++.h> 2 #define ll long long 3 #define N 100010 4 using namespace std; 5 ll n,m,sq,num,_re; //每一块有sq个数字,有num个整块 6 bool yes; //所有块是否为整块,false说明有多余的数字 7 ll a[N]; //原数组 8 ll tag[N],sum[N]; //tag是lazy标记,sum是这一个分块的和 9 ll _a,_x,_y,_k; 10 bool ls(ll x); 11 bool rs(ll x); 12 ll ln(ll x); 13 ll rn(ll x); 14 void update(ll x,ll y,ll z); //x到y所有数均加上k 15 ll query(ll x,ll y); //查询x到y所有数的和 16 ll belong(ll x); 17 int main(){ 18 scanf("%lld%lld",&n,&m); 19 for(int i=1;i<=n;i++){ 20 scanf("%lld",&a[i]); 21 } 22 sq=sqrt(n); 23 num=n/sq; 24 _re=n%sq; 25 if(n%sq==0){ 26 yes=true; //判断是否所有块都是整块 27 } 28 if(!yes){ 29 for(int i=1;i<=num+1;i++){ 30 if(i==num+1){ 31 for(int j=(i-1)*sq+1;j<=n;j++){ 32 sum[i]+=a[j]; 33 } 34 } 35 else{ 36 for(int j=(i-1)*sq+1;j<=i*sq;j++){ 37 sum[i]+=a[j]; 38 } 39 } 40 } 41 } 42 else{ 43 for(int i=1;i<=num;i++){ 44 for(int j=(i-1)*sq+1;j<=i*sq;j++){ 45 sum[i]+=a[j]; 46 } 47 } 48 } 49 for(int i=1;i<=m;i++){ 50 scanf("%lld",&_a); 51 if(_a==1){ 52 scanf("%lld%lld%lld",&_x,&_y,&_k); 53 update(_x,_y,_k); 54 continue; 55 } 56 if(_a==2){ 57 scanf("%lld%lld",&_x,&_y); 58 printf("%lld ",query(_x,_y)); 59 } 60 } 61 return 0; 62 } 63 inline bool ls(ll x){ 64 return (x-1)%sq==0?true:false; 65 } 66 inline bool rs(ll x){ 67 return x%sq==0?true:false; 68 } 69 inline ll ln(ll x){ //包含的第一个整块的下标 70 return rs(x)?((x/sq)+1):(ls(x)?((x/sq)+1):(x/sq)+2); 71 } 72 inline ll rn(ll x){ //包含的最后一个整块的下标 73 return x/sq; 74 } 75 void update(ll x,ll y,ll z){ 76 ll left=sq*(ln(x)-1)+1,right=sq*rn(y); 77 ll be; 78 if(x<left){ 79 be=belong(x); 80 for(int i=x;i<left;i++){ 81 a[i]+=z; 82 sum[be]+=z; 83 } 84 } 85 if(y>right){ 86 be=belong(y); 87 for(int i=right+1;i<=y;i++){ 88 a[i]+=z; 89 sum[be]+=z; 90 } 91 } 92 ll ss=z*sq; 93 ll _l=ln(x),_r=rn(y); 94 for(int i=ln(x);i<=rn(y);i++){ 95 sum[i]+=ss; 96 tag[i]+=z; 97 } 98 return ; 99 } 100 ll query(ll x,ll y){ 101 ll left=sq*(ln(x)-1)+1,right=sq*rn(y); 102 ll _l=ln(x),_r=rn(y); 103 ll esp=0; 104 if(x<left){ 105 for(int i=x;i<left;i++){ 106 esp+=a[i]; 107 } 108 esp+=tag[belong(x)]*(left-x); 109 } 110 if(y>right){ 111 for(int i=right+1;i<=y;i++){ 112 esp+=a[i]; 113 } 114 esp+=tag[belong(y)]*(y-right); 115 } 116 for(int i=ln(x);i<=rn(y);i++){ 117 esp+=sum[i]; 118 } 119 return esp; 120 } 121 ll belong(ll x){ 122 if(x%sq==0){ 123 return x/sq; 124 } 125 else{ 126 return x/sq+1; 127 } 128 }
但这个分块貌似写起来有些问题,写的时候也比较随意。虽然洛谷AC了但我还是本着求真务实的态度上loj测了一遍相同的题。
果然。。改完数组大小后爆零了
可能不是算法速度的问题而是我写的问题。
总之我想说明的是线段树是最nb的!
总之本来我只是想引出线段树的算法优秀性的
所以我上面这个坑我也就不填了,就在这放着qwq
好的下面来讲线段树
接着讲线段树:
先讲讲线段树的性质:
·①线段树是一棵二叉搜索树
·②线段树不一定是一个完全二叉树,如[1,6]这样的线段树就不是完全二叉树(完全二叉树的定义自行百度)
·③线段树是用树形结构来存储数据的,修改和查询区间的时间复杂度均为O(logn),建树的时间复杂度为O(n)
·④线段树是一棵二叉树,线段树上任何结点的度均为0或2,线段树上没有度为1的结点(下方会有证明)
·⑤如果一列数据的数据数目为N,则所创建的线段树的总结点数为2*N-1(下方会有证明)
·⑥线段树是一棵平衡树(本人没学过平衡树,此处挖坑)
好的我们来证明一下性质④和性质⑤:
证明性质④:
假设线段树上有度为1的结点A,那么说明结点A只有一个子结点。说明结点A的所包含的元素的长度为1,那么说明A就是叶子节点,不可能有子结点。所以线段树上不可能有度为1的结点。
证明性质⑤:
令线段树中度为i的结点的数目为ni。
则有n总==n0+n1+n2
由④有n1==0
又易得n0==N,2*n2==n0
代入可得到n总==2*n0-1
即得到结论⑤
继续讲解线段树:
根据线段树的性质我们得知线段树是一棵二叉搜索树,其实质是通过树状结构来储存分成的各个区间的数据并根据树状结构的回溯性来高效的储存,修改,以及访问数据的值。
具体过程看懂上面推荐的题解后应该就能明白了。
大概过程就是:
1.通过某种过程将该数据分割成可以在树上储存的形式,建树
2.通过树的回溯性修改数据
3.借助tag标记和树状结构储存数据的优异性来查询区间数据
过程总结起来就是这样了,虽然ROS学的时候理解了好久。
然后来讲一下各个函数的作用:
·build函数:毫无疑问是建树的
1 void build(ll l,ll r,ll p){ 2 if(l==r){ 3 tr[p]=a[l]; 4 return ; 5 } 6 ll mid=(l+r)>>1; 7 build(l,mid,ls(p)); 8 build(mid+1,r,rs(p)); 9 push_up(p); 10 return ; 11 }
·push_up函数:将此处的两个儿子结点的值更新到此结点上
1 void push_up(ll p){ 2 tr[p]=tr[ls(p)]+tr[rs(p)]; 3 return ; 4 }
·f函数:更新此节点值并将本需要在其下所有结点更新的值记录在此节点的tag上
1 void f(ll p,ll l,ll r,ll k){ 2 tag[p]+=k; 3 tr[p]+=k*(r-l+1); 4 return ; 5 }
·push_down函数:将下方的点的值进行更新
1 void push_down(ll p,ll l,ll r){ 2 ll mid=(l+r)>>1; 3 f(ls(p),l,mid,tag[p]); 4 f(rs(p),mid+1,r,tag[p]); 5 tag[p]=0; 6 }
上总代码:
#include<bits/stdc++.h> //首次背写线段树标程 #define ll long long #define N 100010 using namespace std; ll a[N],tr[4*N],tag[4*N]; ll n,m; ll _a,_b,_c,_d; inline ll ls(ll x){ return x<<1; } inline ll rs(ll x){ return x<<1|1; } void push_up(ll p){ tr[p]=tr[ls(p)]+tr[rs(p)]; return ; } void build(ll l,ll r,ll p){ if(l==r){ tr[p]=a[l]; return ; } ll mid=(l+r)>>1; build(l,mid,ls(p)); build(mid+1,r,rs(p)); push_up(p); return ; } void f(ll p,ll l,ll r,ll k){ tag[p]+=k; tr[p]+=k*(r-l+1); return ; } void push_down(ll p,ll l,ll r){ ll mid=(l+r)>>1; f(ls(p),l,mid,tag[p]); f(rs(p),mid+1,r,tag[p]); tag[p]=0; } inline void do_it(ll p,ll l,ll r){ //如果该节点是叶子结点,则将tag清零防止出错(实际上无影响) if(l==r){ tag[p]=0; } } inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k){ // do_it(p,l,r); //如果该节点是叶子结点,则将tag清零防止出错(实际上无影响) if(nl <= l && r <= nr){ tr[p]+=k*(r-l+1); tag[p]+=k; return ; } ll mid=(l+r)>>1; push_down(p,l,r); if(nl<=mid){ update(nl,nr,l,mid,ls(p),k); } if(nr>mid){ update(nl,nr,mid+1,r,rs(p),k); } push_up(p); return ; } ll query(ll nl,ll nr,ll l,ll r,ll p){ ll tmp=0; if(nl<=l&&r<=nr){ return tr[p]; } ll mid=(l+r)>>1; push_down(p,l,r); if(nl<=mid){ tmp+=query(nl,nr,l,mid,ls(p)); } if(nr>mid){ tmp+=query(nl,nr,mid+1,r,rs(p)); } return tmp; } int main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); } build(1,n,1); for(int i=1;i<=m;i++){ scanf("%lld",&_a); if(_a==1){ scanf("%lld%lld%lld",&_b,&_c,&_d); update(_b,_c,1,n,1,_d); } if(_a==2){ scanf("%lld%lld",&_b,&_c); printf("%lld ",query(_b,_c,1,n,1)); } } return 0; }