浅析:线段树(Segment Tree)
可能我终于算是算法入门了,终于学到了线段树了,加油加油
线段树简介 说到线段树嘛,他就是形如下面这样的一颗二叉树 ,它一般用来维护对于一个序列的各种操作(比如让区间每个数加上一个数 等等) 其中每个节点可以维护一段区间的某些值(比如最值、区间和、区间乘积、异或和 等等满足结合律的操作 。。。),而每个叶子节点都是一个长度为1的区间 ,也就代表了序列本身对应下标的值
线段树操作的复杂度:
建树:$O(nlogn)$
一次区间操作:通常为$O(logn)$
常规线段树 参考blog:
建树 对于一般的线段树,我通常会开一个结构体 来存储树的各种信息
注意事项:
struct Stree { int l,r,sum,lz; }tr[N<<2 ];
那么开始建树
inline void build (int i,int l,int r) { tr[i].l=l,tr[i].r=r,tr[i].lz=0 ; if (l==r){ tr[i].sum=a[l]; return ; } int mid=(l+r)>>1 ; build (i<<1 ,l,mid); build (i<<1 |1 ,mid+1 ,r); tr[i].sum=tr[i<<1 ].sum+tr[i<<1 |1 ].sum; return ; }
单点修改 是树状数组不香了吗?
这个就很简单了,直接像二分一样去找到这个下标所在的节点就好啦!
inline void modify (int i,int k,int x) { if (tr[i].l==tr[i].r){ tr[i].sum+=x; return ; } if (tr[i<<1 ].r>=k) modify (i<<1 ,k,x); else modify (i<<1 |1 ,k,x); tr[i].sum=tr[i<<1 ].sum+tr[i<<1 |1 ].sum; return ; }
区间加法(减法) 对于一个序列,我们需要让[l,r]
区间内所有数+k,那么怎么操作呢?
显然如果我们执行(r-l+1)次单点修改的话,时间绝对会炸,因此我们需要更聪明的办法,这个办法就是之前提到的懒惰标记
当我们需要给一个区间的数都加上k时,如果线段树的当前节点包含在这个区间里面,那么我们肯定是要先把这个节点维护的区间和加上(区间长度)*k
的值,这是毫无疑问的,但他的儿子区间都要改啊,这太麻烦了,反正现在也不用求答案,倒不如先不对这个区间的儿子进行操作了,先给它打上一个“+k”的标记,我们之后要是要求求和再把这个标记下放 到儿子们的区间,那时候再操作也不迟~
这个方法好聪明啊!因此我们在区间修改和查询的时候都需要把懒惰标记下放 ,同时要让父亲的懒惰标记清零
inline void add (int i,int l,int r,int k) { if (tr[i].l>=l&&tr[i].r<=r){ tr[i].sum+=(tr[i].r-tr[i].l+1 )*k; tr[i].lz+=k; return ; } push_down (i); if (tr[i<<1 ].r>=l) add (i<<1 ,l,r,k); if (tr[i<<1 |1 ].l<=r) add (i<<1 |1 ,l,r,k); tr[i].sum=tr[i<<1 ].sum+tr[i<<1 |1 ].sum; return ; }
那么这个下放操作 怎么写呢?
对于最简单的区间加法 ,下放操作就是把加法标记下放给他儿子,同时更新它儿子所维护的区间和就行了
inline void push_down (int i) { if (tr[i].lz==0 ) return ; tr[i<<1 ].lz+=tr[i].lz; tr[i<<1 |1 ].lz+=tr[i].lz; tr[i<<1 ].sum+=(tr[i<<1 ].r-tr[i<<1 |1 ].l+1 )*tr[i].lz; tr[i<<1 |1 ].sum+=(tr[i<<1 ].r-tr[i<<1 |1 ].l+1 )*tr[i].lz; tr[i].lz=0 ; return ; }
询问区间和 对于询问区间和操作,其实就很简单了,如果线段树当前节点对应区间被包含在了询问的区间里,那么直接返回该节点对应的sum即可,否则,就递归地去找左子树和右子树,更新答案(对了,别忘记下放标记,因为这个标记很懒,你不手动下放他是不会动的,哈哈哈~)
inline ll getsum (int i,int l,int r) { if (tr[i].l>=l&&tr[i].r<=r) return tr[i].sum; push_down (i); ll res=0 ; if (tr[i<<1 ].r>=l) res+=getsum (i<<1 ,l,r); if (tr[i<<1 |1 ].l<=r) res+=getsum (i<<1 |1 ,l,r); return res; }
维护区间最值 对于这个操作,我们可以像维护区间和一样维护,而修改和下放操作也需要加上对应的代码
inline void build (int i,int l,int r) { ... tr[i].mx=max (tr[i<<1 ].mx,tr[i<<1 |1 ].mx); tr[i].mn=min (tr[i<<1 ].mn,tr[i<<1 |1 ].mn); return ; }inline void push_down (int i) { ... tr[i<<1 ].mx+=tr[i].lz; tr[i<<1 |1 ].mx+=tr[i].lz; tr[i<<1 ].mn+=tr[i].lz; tr[i<<1 |1 ].mn+=tr[i].lz; ... }
区间乘法+加法(减法) 模板题 线段树2
单独一个区间乘法就很简单了,只需要把上面的懒惰标记改成乘,加号都改为乘号就行了,十分简单,但是如果是乘法和加法 同时出现的话,就没这么简单了
因为,我们需要考虑到,对于懒惰标记的操作,到底是先乘再加 还是 先加再乘?
事实表明,先乘再加 是最好的解法,我们需要维护两个标记(加法标记and乘法标记),在下放标记的时候,只需要将乘法标记直接下放,而加法标记需要先乘以乘法标记再加上下放的加法标记 ,然后更新一下儿子们维护的区间和(同上)就行了
下放操作:
inline void push_down (int i) { int k1=tr[i].plz,k2=tr[i].mlz; tr[i<<1 ].sum=(tr[i<<1 ].sum*k2+k1*(tr[i<<1 ].r-tr[i<<1 ].l+1 ))%p; tr[i<<1 |1 ].sum=(tr[i<<1 ].sum*k2+k1*(tr[i<<1 |1 ].r-tr[i<<1 |1 ].l+1 ))%p; tr[i<<1 ].plz=(tr[i<<1 ].plz*k2+k1)%p; tr[i<<1 |1 ].plz=(tr[i<<1 |1 ].plz*k2+k1)%p; tr[i<<1 ].mlz=(tr[i<<1 ].mlz*k2)%p; tr[i<<1 |1 ].mlz=(tr[i<<1 |1 ].mlz*k2)%p; tr[i].mlz=1 ; tr[i].plz=0 ; return ; }
对于区间修改操作,区间加法我们可以无脑直接加,而区间乘法则需要同时把加法标记乘以这个数k
修改操作:([l,r]
内每个数加上、乘以k)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 inline void add (int i,int l,int r,int k) { if (tr[i].l>=l&&tr[i].r<=r){ tr[i].sum=(tr[i].sum+k*(tr[i].r-tr[i].l+1 ))%p; tr[i].plz=(tr[i].plz+k)%p; return ; } ... }inline void mul (int i,int l,int r,int k) { if (tr[i].l>=l&&tr[i].r<=r){ tr[i].sum=(tr[i].sum*k)%p; tr[i].plz=(tr[i].plz*k)%p; tr[i].mlz=(tr[i].mlz*k)%p; return ; } ... }
区间除法&区间开根号 区间开根号模板题:花神游历各国
对于这两种操作,显然他们不满足结合律 ,但是,这样我们就不能线段树了吗?
大错特错!我们来考虑除法与开根号这些运算的特性:他们都会使得一堆元素(整数)不断地减小,并且趋于一个值(除法趋于0,开根号趋于1)
针对除法来说:对于一个区间的所有元素而言,如果这个区间内元素最大值与他除以除数所得商的差值 等于 最小值与他除以除数所得商的差值 ,那么这个区间内的所有元素除以除数都会减去同一个数,于是就可以给这个区间的加法标记 做一点操作,就可以把区间除法转换成区间减法啦!
同理,如果这个区间的最大值与它开根号的差值 等于 最小值与它开根号的差值 ,那么这个区间内的所有元素开根号都会减去同一个数,我们就能把开根号转换为减法啦!
所以说,对于区间除法和区间开根号操作,他们都可以转化为区间减法 操作来优化复杂度(这十分重要,否则就是一个单纯的暴力了呀~)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 inline void Sqrt (int i,int l,int r) { if (tr[i].l>=l&&tr[i].r<=r){ int k1=tr[i].mx-floor (sqrt (tr[i].mx)); int k2=tr[i].mn-floor (sqrt (tr[i].mn)); if (k1==k2){ tr[i].lz-=k1; tr[i].sum-=k1*(tr[i].r-tr[i].l+1 ); tr[i].mx-=k1; tr[i].mn-=k1; return ; } } ... }inline void div (int i,int l,int r,int k) { ... if (tr[i].l>=l&&tr[i].r<=r){ int k1=tr[i].mx-tr[i].mx/k; int k2=tr[i].mn-tr[i].mn/k; if (k1==k2){ tr[i].sum-=k1*(tr[i].r-tr[i].l+1 ); tr[i].lz-=k1; tr[i].mx-=k1; tr[i].mn-=k1; } } ... }
然而,对于区间开根号操作,还有更好的办法:对于一个很大的数,我们只要不断对其开根号,用不了几次它就会变成1,那么我们可以通过维护区间最大值是否小于等于1来判断这个区间是否有必要操作 ,这样就可以大大优化时间复杂度
inline void Sqrt (int i,int l,int r) { if (tr[i].l==tr[i].r){ tr[i].sum=tr[i].mx=sqrt (tr[i].sum); return ; } push_down (i); if (tr[i<<1 ].r>=l&&tr[i<<1 ].mx>1 ) Sqrt (i<<1 ,l,r); if (tr[i<<1 |1 ].l<=r&&tr[i<<1 |].mx>1 ) Sqrt (i<<1 |1 ,l,r); tr[i].sum=tr[i<<1 ].sum+tr[i<<1 |1 ].sum; tr[i].mx=max (tr[i<<1 ].mx,tr[i<<1 |1 ].mx); }
UPD 2020.6.8
这几天学校训练做了一道类似的题USST训练赛9 时间管理 ,题意大概是区间gcd修改+区间求和 ,对于区间gcd,可以同样用线段数(或分块)维护区间的值是否相同,若相同,直接$O(1)$修改该区间和即可,因为gcd操作和开根号操作一样,总是让一段区间趋于1的
线段树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 ll a[maxn],n,m;struct SegmentTree { #define ls i<<1 #define rs i<<1|1 struct Tree { int l,r; ll sum,ptg,minh,maxh; }tr[maxn<<2 ]; inline void push_up (int i) { tr[i].sum=tr[ls].sum+tr[rs].sum; tr[i].minh=min (tr[ls].minh,tr[rs].minh); tr[i].maxh=max (tr[ls].maxh,tr[rs].maxh); } inline void push_down (int i) { if (tr[i].ptg==0 ) return ; ll k=tr[i].ptg; tr[ls].sum+=k*(tr[ls].r-tr[ls].l+1 ); tr[rs].sum+=k*(tr[rs].r-tr[rs].l+1 ); tr[ls].minh+=k; tr[ls].maxh+=k; tr[rs].minh+=k; tr[rs].maxh+=k; tr[ls].ptg+=k; tr[rs].ptg+=k; tr[i].ptg=0 ; } inline void build (int i,int l,int r) { tr[i].l=l,tr[i].r=r; tr[i].ptg=0 ; if (tr[i].l==tr[i].r){ tr[i].sum=a[l]; tr[i].minh=a[l]; tr[i].maxh=a[l]; return ; } int mid=(l+r)>>1 ; build (ls,l,mid); build (rs,mid+1 ,r); push_up (i); } inline void change (int i,int l,int r,ll k) { if (tr[i].l>=l&&tr[i].r<=r){ if (tr[i].minh==tr[i].maxh){ ll tp=gcd (tr[i].minh,k)-tr[i].minh; tr[i].sum+=tp*(tr[i].r-tr[i].l+1 ); tr[i].ptg+=tp; tr[i].minh+=tp; tr[i].maxh+=tp; return ; } } push_down (i); if (tr[ls].r>=l) change (ls,l,r,k); if (tr[rs].l<=r) change (rs,l,r,k); push_up (i); } inline ll getsum (int i,int l,int r) { if (tr[i].l>=l&&tr[i].r<=r) return tr[i].sum; push_down (i); ll res=0 ; if (tr[ls].r>=l) res+=getsum (ls,l,r); if (tr[rs].l<=r) res+=getsum (rs,l,r); return res; } }T;int main () { read (n),read (m); rep (i,1 ,n) read (a[i]); T.build (1 ,1 ,n); int op,l,r,x; rep (i,1 ,m){ read (op),read (l),read (r); if (op&1 ){ read (x); T.change (1 ,l,r,x); }else { printf ("%lld\n" ,T.getsum (1 ,l,r)); } } return 0 ; }
分块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 ll a[maxn],n,m,blk,bel[maxn],sz[maxn],st[maxn],en[maxn],tg[maxn]; ll minh[maxn],maxh[maxn]; ll sum[maxn];inline void init_blk () { blk=sqrt (n); rep (i,1 ,blk){ st[i]=n/blk*(i-1 )+1 ; en[i]=n/blk*i; minh[i]=inf; maxh[i]=-inf; } en[blk]=n; rep (i,1 ,blk){ sz[i]=en[i]-st[i]+1 ; rep (j,st[i],en[i]){ bel[j]=i; minh[i]=min (minh[i],a[j]); maxh[i]=max (maxh[i],a[j]); } } }inline void change (ll l,ll r,ll x) { if (bel[l]==bel[r]){ for (int i=l;i<=r;i++){ a[i]=gcd (a[i]+tg[bel[l]],x); } minh[bel[l]]=inf,maxh[bel[l]]=-inf; for (int i=st[bel[l]];i<=en[bel[l]];i++){ if (i<l||i>r) a[i]+=tg[bel[l]]; minh[bel[l]]=min (minh[bel[l]],a[i]), maxh[bel[l]]=max (maxh[bel[l]],a[i]); } tg[bel[l]]=0 ; }else { for (int i=l;i<=en[bel[l]];i++){ a[i]=gcd (a[i]+tg[bel[l]],x); } minh[bel[l]]=inf,maxh[bel[l]]=-inf; for (int i=st[bel[l]];i<=en[bel[l]];i++){ if (i<l) a[i]+=tg[bel[l]]; minh[bel[l]]=min (minh[bel[l]],a[i]), maxh[bel[l]]=max (maxh[bel[l]],a[i]); } tg[bel[l]]=0 ; for (int i=st[bel[r]];i<=r;i++){ a[i]=gcd (a[i]+tg[bel[r]],x); } minh[bel[r]]=inf,maxh[bel[r]]=-inf; for (int i=st[bel[r]];i<=en[bel[r]];i++){ if (i>r) a[i]+=tg[bel[r]]; minh[bel[r]]=min (minh[bel[r]],a[i]), maxh[bel[r]]=max (maxh[bel[r]],a[i]); } tg[bel[r]]=0 ; for (int i=bel[l]+1 ;i<=bel[r]-1 ;i++){ if (minh[i]==maxh[i]){ int tp=gcd (minh[i],x); tg[i]+=tp-minh[i]; minh[i]=maxh[i]=tp; }else { for (int j=st[i];j<=en[i];j++){ a[j]=gcd (a[j]+tg[i],x); } minh[i]=inf,maxh[i]=-inf; for (int j=st[i];j<=en[i];j++){ minh[i]=min (minh[i],a[j]), maxh[i]=max (maxh[i],a[j]); } tg[i]=0 ; } } } }inline ll getsum (ll l,ll r) { ll res=0 ; if (bel[l]==bel[r]){ for (int i=l;i<=r;i++){ res+=(a[i]+tg[bel[l]]); } }else { for (int i=l;i<=en[bel[l]];i++){ res+=(a[i]+tg[bel[l]]); } for (int i=st[bel[r]];i<=r;i++){ res+=(a[i]+tg[bel[r]]); } for (int i=bel[l]+1 ;i<=bel[r]-1 ;i++){ if (minh[i]==maxh[i]){ res+=(sz[i]*minh[i]); }else { for (int j=st[i];j<=en[i];j++){ res+=(a[j]+tg[i]); } } } } return res; }int main () { read (n),read (m); rep (i,1 ,n) read (a[i]); init_blk (); int op,l,r,x; rep (i,1 ,m){ read (op),read (l),read (r); if (op&1 ){ read (x); change (l,r,x); }else { printf ("%lld\n" ,getsum (l,r)); } } return 0 ; }
Lazy Tag的用途 来源于这道典型例题:Mayor’s posters (线段树+离散化)
就像铺地毯一样,我们不停地在某些区间上铺一些地毯,最后问能在整个区间看到多少种颜色的地毯?
这题我们离线做(动态开点目前没学orz),首先离散化三部曲(排序 去重 二分插入,否则你会MLE)我就不多说了,那么这个题怎么用Lzay Tag来维护呢,我们很容易想到整个区间的种类数比较好求,只要开一个vis数组记录就行了,而这个离线的区间更新怎么做呢?我们思考区间求和的懒惰标记是维护的求和的加数,但是我们这是维护一个颜色(即种类),如果一个区间的子区间的颜色在后来被修改了,那么我们就需要把这个区间的lazytag下放,直到给到那个被修改的子区间,然后把那个修改的子区间的颜色改成要修改成的颜色,这样一来,维护了这个区间其他子区间颜色没变,而只改变了这一个区间的颜色,非常巧妙
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N=1e5 +5 ; ll t,n,tot,l,r,vis[N],b[N];struct node { ll l,r; }a[N];struct Stree { ll l,r,col; }tree[N<<2 ];inline void init () { memset (b,0 ,sizeof (b)); memset (vis,0 ,sizeof (vis)); ll temp=0 ; scanf ("%lld" ,&n); for (int i=1 ;i<=n;i++){ scanf ("%lld%lld" ,&a[i].l,&a[i].r); b[++temp]=a[i].l,b[++temp]=a[i].r; } sort (b+1 ,b+1 +temp); tot=unique (b+1 ,b+1 +temp)-b-1 ; for (int i=1 ;i<=n;i++){ a[i].l=lower_bound (b+1 ,b+1 +tot,a[i].l)-b; a[i].r=lower_bound (b+1 ,b+1 +tot,a[i].r)-b; } }inline void build (ll i,ll l,ll r) { tree[i].l=l,tree[i].r=r,tree[i].col=0 ; if (l==r){ return ; } ll mid=(l+r)>>1 ; build (i<<1 ,l,mid); build (i<<1 |1 ,mid+1 ,r); return ; }inline void push_down (ll i) { if (tree[i].col==0 ) return ; tree[i<<1 ].col=tree[i<<1 |1 ].col=tree[i].col; tree[i].col=0 ; return ; }inline void update (ll i,ll l,ll r,ll col) { if (tree[i].l>=l&&tree[i].r<=r){ tree[i].col=col; return ; } push_down (i); if (tree[i<<1 ].r>=l) update (i<<1 ,l,r,col); if (tree[i<<1 |1 ].l<=r) update (i<<1 |1 ,l,r,col); }inline void search (ll i,ll l,ll r,ll &ans) { if (tree[i].col){ if (!vis[tree[i].col]){ vis[tree[i].col]=1 ; ans++; } return ; } search (i<<1 ,l,r,ans); search (i<<1 |1 ,l,r,ans); return ; }inline ll ask (ll l,ll r) { ll ans=0 ; search (1 ,l,r,ans); return ans; }int main () { scanf ("%lld" ,&t); while (t--){ init (); build (1 ,1 ,tot); ll col=0 ; for (int i=1 ;i<=n;i++){ update (1 ,a[i].l,a[i].r,++col); } printf ("%lld\n" ,ask (1 ,tot)); } return 0 ; }
权值线段树 例题:洛谷 P1801 黑匣子 (离散化+权值线段树)
与一般的线段树有一点区别:权值线段树的每个叶子节点就代表了数列中一个具体的值 (我们称之为权值 ),而每个叶子节点维护的是这个具体的值在整个数列中出现的次数 ,每一个父亲节点维护了一段区间的值出现的总次数 ,这样一来,权值线段树的意义就很明朗了
权值线段树的一个重要应用:求整个数列中第k大(第k小)的数 (注意是整个数列)
算法:权值线段树的基本操作和普通线段树几乎相同,需要注意的是以下几点:
每个节点维护的不再是权值or区间和,而是该节点代表数字出现的次数
比如我们可以开一个结构体存储
struct Stree { int l,r; int v; }tr[N<<2 ];
更新操作(比如插入了一个数),就是递归找到该数对应的叶子节点,给节点维护的次数+1
询问操作(以询问第k小为例),递归地找,如果左子树内有大于等于k个数字,那么就递归进入左子树找第k小,否则就递归进入右子树找第k-tr[rs].v
小
例题代码:
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <bits/stdc++.h> using namespace std;#define ls (i<<1) #define rs (i<<1|1) const int N=2e5 +5 ;int n,m,b[N],c[N],cnt=1 ;struct node { int val,id; }a[N];bool cmp (node x1,node x2) { return x1.val<x2.val; }struct Stree { int l,r,v; }tr[N<<2 ];inline void build (int i,int l,int r) { tr[i].l=l,tr[i].r=r; if (l==r) return ; int mid=(l+r)>>1 ; build (ls,l,mid); build (rs,mid+1 ,r); }inline void update (int i,int x) { if (tr[i].l==tr[i].r){ tr[i].v++; return ; } if (tr[ls].r>=x) update (ls,x); else update (rs,x); tr[i].v=tr[ls].v+tr[rs].v; }inline int getval (int i,int k) { if (tr[i].l==tr[i].r){ return tr[i].l; } if (tr[ls].v>=k) return getval (ls,k); else return getval (rs,k-tr[ls].v); }int main () { scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=n;i++) scanf ("%d" ,&a[i].val),a[i].id=i; sort (a+1 ,a+1 +n,cmp); for (int i=1 ;i<=n;i++) c[a[i].id]=i; for (int i=1 ;i<=m;i++) scanf ("%d" ,&b[i]); build (1 ,1 ,n); for (int i=1 ;i<=n;i++){ update (1 ,c[i]); while (b[cnt]==i){ printf ("%d\n" ,a[getval (1 ,cnt)].val); cnt++; } } return 0 ; }
动态开点线段树 参考blog:
这东西就比较神了,它是线段树的(阉割)简化版,原理就是:我们不会在询问出现之前就利用build函数建立一颗完整的满二叉树 结构的线段树(因为这样可能会有很多节点的空间没有用上,浪费了空间),而是采用边询问边建树 的方式动态开点 ,这样以来,不用的节点我们不开,用的节点我们用的时候再开,可以很高效地处理值域较大(1e9),但是操作次数较少(1e5) 的操作,或是需要建立多棵独立的线段树 的时候
通常情况下,我们为动态开点线段树开(修改次数*40) 的空间(类似主席树),然后根据RE和MLE来修改空间,其余特征和普通线段树无差异(pushdown下放,pushup更新)
注意事项:
我们不再开结构体(可以但没必要)存树,因为它不再满足儿子节点为根节点编号的两倍和两倍加一原则(不是满二叉树),而是利用一个时间戳编号 来存储节点编号
也不需要存储每个节点所对应的区间左右端点值(其实普通线段树也没必要存)
const int N=1e5 +5 ;int ls[N*40 ],rs[N*40 ],tg[N*40 ],rt,ncnt; ll sum[N*40 ];
下面放线段树1 的动态开点形式:
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int N=1e5 +5 ;int n,m,op,x,y,k,ls[N*40 ],rs[N*40 ],tg[N*40 ],rt,ncnt; ll sum[N*40 ];inline void pushup (int o) { sum[o]=sum[ls[o]]+sum[rs[o]]; }inline void init (int &o,int l,int r,int x,int k) { if (!o) o=++ncnt; if (l==r){ sum[o]+=1ll *k; return ; } int mid=(l+r)>>1 ; if (mid>=x) init (ls[o],l,mid,x,k); else init (rs[o],mid+1 ,r,x,k); pushup (o); }inline void push_down (int o,int l,int r) { if (!tg[o]) return ; if (!ls[o]) ls[o]=++ncnt; if (!rs[o]) rs[o]=++ncnt; int mid=(l+r)>>1 ; sum[ls[o]]+=tg[o]*(mid-l+1 ); sum[rs[o]]+=tg[o]*(r-mid); tg[ls[o]]+=tg[o]; tg[rs[o]]+=tg[o]; tg[o]=0 ; return ; }inline void update (int &o,int l,int r,int L,int R,int k) { if (!o) o=++ncnt; if (l>=L&&r<=R){ sum[o]+=1ll *k*(r-l+1 ); tg[o]+=k; return ; } push_down (o,l,r); int mid=(l+r)>>1 ; if (mid>=L) update (ls[o],l,mid,L,R,k); if (mid+1 <=R) update (rs[o],mid+1 ,r,L,R,k); pushup (o); }inline ll getsum (int &o,int l,int r,int L,int R) { if (!o) return 0 ; if (l>=L&&r<=R) return sum[o]; push_down (o,l,r); ll res=0 ; int mid=(l+r)>>1 ; if (mid>=L) res+=getsum (ls[o],l,mid,L,R); if (mid+1 <=R) res+=getsum (rs[o],mid+1 ,r,L,R); return res; }int main () { scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=n;i++){ scanf ("%d" ,&x); init (rt,1 ,n,i,x); } for (int i=1 ;i<=m;i++){ scanf ("%d%d%d" ,&op,&x,&y); if (op==1 ){ scanf ("%d" ,&k); update (rt,1 ,n,x,y,k); }else { printf ("%lld\n" ,getsum (rt,1 ,n,x,y)); } } return 0 ; }
可持久化线段树(主席树) 题目链接:P3834 【模板】可持久化线段树 1(主席树)
参考blog:
题意:给定$n$个整数构成的序列,将对于指定的闭区间查询其区间内的第$k$小值
经典得不能再经典的区间$Kth$问题,即(强制在线的)询问若干区间内的 $k$大/$k$小 值,对于这类问题,当然可以使用暴力$O(nq)$,但是你会T得很惨,于是便又出现了一种神仙数据结构——可持久化线段树 (或 主席树,据说是因为发明人的名字缩写叫hjt)
其实主席树 并不是什么新的数据结构,本质上它其实是权值线段树+前缀和思想 应用的结合,原理描述起来也十分简单,即:开$n$棵权值线段树,第$i$棵维护序列的前$i$个数的出现次数,对于每一次$[l,r]$的询问,用第$r$棵线段树减去第$l-1$棵线段树,得出的新线段树就是维护了$[l,r]$区间的每个数的出现次数的权值线段树,然后在这棵树上二分求解$Kth$就行了,但是,仅仅这样做会使得空间特别大导致RE,因此需要优化,优化过后的这些树就是主席树啦,那么如何进行空间优化呢?
我们发现,对于每一次插入新的序列中的值时,都构建出一颗完整的权值线段树是不值得的,因为有很多的点早就已经在之前构建的权值线段树中出现过了,且没有发生改变,这些点没必要再重新新建,直接利用之前构建好的点就行了,因此,单次的更新只需要新建$log(n)$个新节点即可 ,空间大大缩减
举个栗子:4 3 2 3 6 1
初始时,只有一棵空的线段树,所有节点对应的出现次数为0,当加入区间$[1,1]$时,即 将4的出现次数+1,此时,只需要将与4节点有关的节点维护的次数值+1即可,剩余的节点不需要改变,直接连回去即可
插入$[1,3]$区间后得到的线段树如下图所示:
接下来是代码实现
一些变量 T[maxn*50]
:存储每棵线段树的根节点
ls[maxn*50]
:存储左儿子编号
rs[maxn*50]
:存储右儿子编号
ci[maxn*50]
:记录节点对应权值(出现次数)
tcnt
:节点个数
离散化 通常情况下,如果要求$Kth$,给出的数值一般都比$n$大很多,所以需要离散化,否者会炸空间RE哦
rep (i,1 ,n) read (a[i]),b[i]=a[i];sort (b+1 ,b+1 +n);int len=unique (b+1 ,b+1 +n)-b-1 ;
Build建树函数 void build (int &rt,int l,int r) { rt=++tcnt,ci[rt]=0 ; if (l==r) return ; int mid=(l+r)>>1 ; build (ls[rt],1 ,mid); build (rs[rt],mid+1 ,r); }
Update更新函数 int update (int o,int l,int r,int val) { int oo=++tcnt; ls[oo]=ls[o],rs[oo]=rs[o],ci[oo]=ci[o]+1 ; if (l==r) return oo; int mid=(l+r)>>1 ; if (mid>=val) ls[oo]=update (ls[oo],l,mid,val); else rs[oo]=update (rs[oo],mid+1 ,r); return oo; }
Query询问函数 int query (int L,int R,int l,int r,int k) { int mid=(l+r)>>1 ; int x=ci[ls[R]]-ci[ls[L]]; if (l==r) return l; if (x>=k) return query (ls[L],ls[R],l,mid,k); else return query (rs[L],rs[R],mid+1 ,r,k-x); }
完整模板题代码 通常情况下,主席树的空间需要开32~50倍的序列长度大小,总之,能开多大开多大吧
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <bits/stdc++.h> using namespace std;#define rep(i,a,b) for(register int i=a;i<=b;i++) #define All(x) (x).begin(),(x).end() #define pb push_back #define mp make_pair typedef long long ll;typedef double db;typedef vector<int > VI;typedef pair<int ,int > PII;const double PI=acos (-1.0 );const double eps=1e-6 ;const long long mod=1e9 +7 ;const int inf=0x7fffffff ;const int maxn=200005 ;ll qpow (ll x,ll y,ll Mod) {ll ans=1 ,base=x%Mod; while (y){if (y&1 )ans=(ans*base)%Mod;base=(base*base)%Mod;y>>=1 ;} return ans;}ll gcd (ll a,ll b) {return b?gcd (b,a%b):a;}template <class T> void read (T &x) { x=0 ;int f=0 ;char ch=getchar (); while (ch<'0' ||ch>'9' ){f|=(ch=='-' );ch=getchar ();} while (ch>='0' &&ch<='9' ){x=(x<<1 )+(x<<3 )+(ch^48 );ch=getchar ();} x=f?-x:x; return ; }int n,m,type,a[maxn],b[maxn],ans;int T[maxn*40 ],ls[maxn*40 ],rs[maxn*40 ],ci[maxn*40 ],tcnt;inline void build (int &rt,int l,int r) { rt=++tcnt,ci[rt]=0 ; if (l==r) return ; int mid=(l+r)>>1 ; build (ls[rt],l,mid); build (rs[rt],mid+1 ,r); }int update (int ort,int l,int r,int val) { int nrt=++tcnt; ls[nrt]=ls[ort],rs[nrt]=rs[ort],ci[nrt]=ci[ort]+1 ; if (l==r) return nrt; int mid=(l+r)>>1 ; if (mid>=val) ls[nrt]=update (ls[nrt],l,mid,val); else rs[nrt]=update (rs[nrt],mid+1 ,r,val); return nrt; }int query (int lt,int rt,int l,int r,int k) { if (l==r) return l; int mid=(l+r)>>1 ,x=ci[ls[rt]]-ci[ls[lt]]; if (x>=k) return query (ls[lt],ls[rt],l,mid,k); else return query (rs[lt],rs[rt],mid+1 ,r,k-x); }int main () { read (n),read (m); rep (i,1 ,n) read (a[i]),b[i]=a[i]; sort (b+1 ,b+1 +n); int len=unique (b+1 ,b+1 +n)-b-1 ; build (T[0 ],1 ,len); rep (i,1 ,n){ int x=lower_bound (b+1 ,b+1 +len,a[i])-b; T[i]=update (T[i-1 ],1 ,len,x); } for (int i=0 ,x,y,k;i<m;i++){ read (x),read (y),read (k); printf ("%d\n" ,b[query (T[x-1 ],T[y],1 ,len,k)]); } return 0 ; }
习题 P3834 【模板】可持久化线段树 1(主席树)
P3567 [POI2014]KUR-Couriers (动态询问区间是否存在出现次数大于一半的数)
UPD:2022.10.3
时光荏苒,一转眼两年半过去了,现在再回过头来看之前学习的算法和数据结构,代码风格实在是不敢苟同(我为什么当时能写得出这么OI风格的代码??)如今加深了对语言和算法的认识,现在重新拾起旧知识的时候,有了很多自己的想法和思考。接下来介绍一下线段树常用小技巧 之 线段树合并 。
线段树合并 参考blog:
首先我们先规定一下线段树的节点结构体:存储了需要维护的值val
、左儿子下标LS
、右儿子下标RS
。
这棵树是由一个vector<Node>
来维护的。
struct Node { T val; int LS, RS; Node (): val (0 ) { LS = 0 , RS = 0 ; } }; vector<Node> tr;
动态开点线段树合并,简称线段树合并,顾名思义,就是将两棵线段树的节点值合并,注意这里的合并是指维护的信息合并,例如:区间和的合并,就是将两个节点的区间和相加。线段树合并的代码十分简单:
如果两棵树对应节点任意一个没有被开辟,则返回另一个节点
如果是叶子节点,将信息合并
递归左右子树
代码如下:
int merge (int a, int b, int l, int r) { if (!a) return b; if (!b) return a; if (l == r){ return a; } int mid = (l + r) >> 1 ; tr[a].LS = merge (tr[a].LS, tr[b].LS, l, mid); tr[a].RS = merge (tr[a].RS, tr[b].RS, mid + 1 , r); push_up (a); return a; }
线段树合并的时间复杂度与树中插入的节点个数有关,如果我们需要加入$k$个点,则会向树中会插入$k \log k$个节点,每次合并两棵线段树同位置的点,就会少掉一个点,这个复杂度是$O(1)$的,因此线段树合并的总复杂度为$O(k\log k)$,具体证明可以参考上方blog。
习题 P3605 USACO17JAN Promotion Counting P
题意:一棵有根树,求每个子树中严格大于当前节点值的节点个数
题解:对每个节点维护一棵动态开点线段树,线段树维护区间和,然后直接dfs + 线段树合并
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int ,int >;#define DYNAMIC template <class T >class SegTree {public : static constexpr T T_MIN = numeric_limits<T>::lowest (); static constexpr T T_MAX = numeric_limits<T>::max (); struct Node { T val; #ifdef DYNAMIC int LS, RS; #endif Node (): val (0 ) { #ifdef DYNAMIC LS = 0 , RS = 0 ; #endif } }; #ifdef DYNAMIC #define ls (tr[i].LS) #define rs (tr[i].RS) int tcnt; #else #define ls (i<<1) #define rs (i<<1|1) #endif vector<Node> tr; void push_down (int i, int l, int r) { #ifdef DYNAMIC if (!ls) ls = ++tcnt, tr.emplace_back (Node{}); if (!rs) rs = ++tcnt, tr.emplace_back (Node{}); #endif } void push_up (int i) { tr[i].val = tr[ls].val + tr[rs].val; } #ifdef DYNAMIC SegTree (): tcnt (1 ), tr (2 ) {} SegTree (const int &N): tcnt (N), tr (N+1 ) {} int merge (int a, int b, int l, int r) { if (!a) return b; if (!b) return a; if (l == r){ tr[a].val += tr[b].val; return a; } int mid = (l + r) >> 1 ; tr[a].LS = merge (tr[a].LS, tr[b].LS, l, mid); tr[a].RS = merge (tr[a].RS, tr[b].RS, mid + 1 , r); push_up (a); return a; } #else void build (int i,int l,int r,const vector<T> &a) { if (l==r){ tr[i].val = a[l]; tr[i].maxh = a[l]; return ; } int mid = (l + r) >> 1 ; build (ls, l, mid, a); build (rs, mid+1 , r, a); push_up (i); } void RESIZE (const int &N) { tr.resize (N<<2 ); } SegTree () = default ; SegTree (const int &N): tr (N<<2 ) {} SegTree (const int &N, const vector<T> &a): tr (N<<2 ) { build (1 , 1 , N, a); } #endif void add_point (int i, int l, int r, int pos, T k) { if (l == r) { tr[i].val += k; return ; } push_down (i, l, r); int mid = (l + r) >> 1 ; if (pos <= mid) add_point (ls, l, mid, pos, k); else add_point (rs, mid+1 , r, pos, k); push_up (i); } T getsum (int i, int l, int r, int L, int R) { if (l >= L && r <= R) return tr[i].val; push_down (i, l, r); int mid = (l + r) >> 1 ; T ret = 0 ; if (mid >= L) ret += getsum (ls, l, mid, L, R); if (mid+1 <= R) ret += getsum (rs, mid+1 , r, L, R); return ret; } };void solve () { int n; cin >> n; vector<vector<int >> e (n+1 ); vector<int > a (n+1 ) ; for (int i=1 ;i<=n;++i) cin >> a[i]; for (int fa,i=2 ;i<=n;++i){ cin >> fa; e[i].emplace_back (fa); e[fa].emplace_back (i); } vector<int > ans (n+1 ) ; SegTree<int > seg (n) ; auto dfs = [&](auto &&self, int u, int fath) -> void { for (auto &&v: e[u]){ if (v == fath) continue ; self (self, v, u); seg.merge (u, v, 1 , 1e9 ); } ans[u] = seg.getsum (u, 1 , 1e9 , a[u]+1 , 1e9 ); seg.add_point (u, 1 , 1e9 , a[u], 1 ); }; dfs (dfs, 1 , -1 ); for (int i=1 ;i<=n;++i) cout << ans[i] << '\n' ; }int main () { cin.tie (nullptr )->sync_with_stdio (false ); int _ = 1 ; while (_--){ solve (); } return 0 ; }
P4556 Vani有约会 雨天的尾巴 /【模板】线段树合并
题意:一棵有根树,m次操作,每次给[x,y]路径上所有点发放z类型的食物,问所有操作后,每个点存放的最多的是哪种食物?
题解:首先,[x,y]路径操作可以转化成树上差分,即x+1, y+1, LCA(x,y)-1, fa[LCA(x,y)]-1
。对每个节点维护一棵动态开点线段树,线段树维护该点的每种食物的出现次数 和 出现次数最大的食物下标。先树上差分将所有修改处理好,然后dfs + 线段树合并,注意合并时需要保证合并好所有信息。(易错点:可能会出现每种食物的出现次数=0 ,但出现次数最大的食物下标≠0 的情况,这里需要特判使得ans = 0)
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 #include <bits/stdc++.h> using namespace std;using ll = long long ;using pii = pair<int ,int >;#define DYNAMIC template <class T >class SegTree {public : static constexpr T T_MIN = numeric_limits<T>::lowest (); static constexpr T T_MAX = numeric_limits<T>::max (); struct Node { T maxh, maxidx; #ifdef DYNAMIC int LS, RS; #endif Node (): maxh (0 ), maxidx (0 ) { #ifdef DYNAMIC LS = 0 , RS = 0 ; #endif } }; #ifdef DYNAMIC #define ls (tr[i].LS) #define rs (tr[i].RS) int tcnt; #else #define ls (i<<1) #define rs (i<<1|1) #endif vector<Node> tr; void push_down (int i, int l, int r) { #ifdef DYNAMIC if (!ls) ls = ++tcnt, tr.emplace_back (Node{}); if (!rs) rs = ++tcnt, tr.emplace_back (Node{}); #endif } void push_up (int i) { if (tr[ls].maxh >= tr[rs].maxh) tr[i].maxh = tr[ls].maxh, tr[i].maxidx = tr[ls].maxidx; else tr[i].maxh = tr[rs].maxh, tr[i].maxidx = tr[rs].maxidx; } #ifdef DYNAMIC SegTree (): tcnt (1 ), tr (2 ) {} SegTree (const int &N): tcnt (N), tr (N+1 ) {} int merge (int a, int b, int l, int r) { if (!a) return b; if (!b) return a; if (l == r){ tr[a].maxh += tr[b].maxh; tr[a].maxidx = l; return a; } int mid = (l + r) >> 1 ; tr[a].LS = merge (tr[a].LS, tr[b].LS, l, mid); tr[a].RS = merge (tr[a].RS, tr[b].RS, mid + 1 , r); push_up (a); return a; } #else void build (int i,int l,int r,const vector<T> &a) { if (l==r){ tr[i].val = tr[i].maxh = a[l]; return ; } int mid = (l + r) >> 1 ; build (ls, l, mid, a); build (rs, mid+1 , r, a); push_up (i); } void RESIZE (const int &N) { tr.resize (N<<2 ); } SegTree () = default ; SegTree (const int &N): tr (N<<2 ) {} SegTree (const int &N, const vector<T> &a): tr (N<<2 ) { build (1 , 1 , N, a); } #endif void add_point (int i, int l, int r, int pos, T k) { if (l == r) { tr[i].maxh += k; tr[i].maxidx = l; return ; } push_down (i, l, r); int mid = (l + r) >> 1 ; if (pos <= mid) add_point (ls, l, mid, pos, k); else add_point (rs, mid+1 , r, pos, k); push_up (i); } };void solve () { int n,m; cin >> n >> m; vector<vector<int >> e (n+1 ); for (int x,y,i=1 ;i<n;++i){ cin >> x >> y; e[x].emplace_back (y); e[y].emplace_back (x); } vector<array<int ,22>> bfa (n+1 ); vector<int > deep (n+1 ) ; auto init_beizeng = [&](int root){ auto dfs = [&](auto &&self, int u,int fa,int dep)->void { deep[u] = dep; bfa[u][0 ] = fa; for (auto &&v:e[u]){ if (v == fa) continue ; self (self, v, u, dep+1 ); } }; dfs (dfs, root, 0 , 0 ); for (int i=1 ;(1 <<i)<=n;++i){ for (int j=1 ;j<=n;++j){ bfa[j][i]=bfa[bfa[j][i-1 ]][i-1 ]; } } }; init_beizeng (1 ); auto LCA = [&](int x,int y){ if (deep[x] < deep[y]) swap (x,y); while (deep[x] > deep[y]){ x=bfa[x][int (log2 (deep[x]-deep[y]))]; } if (x==y) return x; for (int i=log2 (deep[x]); i>=0 ; i--){ if (bfa[x][i]!=bfa[y][i]){ x=bfa[x][i],y=bfa[y][i]; } } return bfa[x][0 ]; }; SegTree<int > seg (n) ; const int N = 100000 ; for (int x,y,z,i=1 ;i<=m;++i){ cin >> x >> y >> z; seg.add_point (x, 1 , N, z, 1 ); seg.add_point (y, 1 , N, z, 1 ); seg.add_point (LCA (x,y), 1 , N, z, -1 ); seg.add_point (bfa[LCA (x,y)][0 ], 1 , N, z, -1 ); } vector<int > ans (n+1 ) ; auto dfs = [&](auto &&self, int u, int fath) -> void { for (auto &&v: e[u]){ if (v == fath) continue ; self (self, v, u); seg.merge (u, v, 1 , N); } if (seg.tr[u].maxh == 0 ) ans[u] = 0 ; else ans[u] = seg.tr[u].maxidx; }; dfs (dfs, 1 , 0 ); for (int i=1 ;i<=n;++i) cout << ans[i] << '\n' ; }int main () { cin.tie (nullptr )->sync_with_stdio (false ); int _ = 1 ; while (_--){ solve (); } return 0 ; }