《zkw 线段树-原理及其扩展》例题
《zkw 线段树-原理及其扩展》例题
我的博客 Tmbcan's Blog
非常简单的模板题没有写讲解,难一些的写了讲解或者注意事项。
难度大致由易到难。
没开网隐,zkwAC记录可查。
例一:Luogu P3372 【模板】线段树 1
const int N = 1e5+10;
int n,m,P=1;
ll tr[N*3],sum[N*3];
inline void change_add(int l,int r,ll k){
int siz = 1;
for(l=P+l-1,r=P+r+1;l^1^r; ){
if(~l&1) tr[l^1]+=siz*k,sum[l^1]+=k;
if(r&1) tr[r^1]+=siz*k,sum[r^1]+=k;
l>>=1; r>>=1; siz<<=1;
tr[l] = tr[l<<1]+tr[l<<1|1]+sum[l]*siz;
tr[r] = tr[r<<1]+tr[r<<1|1]+sum[r]*siz;
}
for(l>>=1,siz<<=1; l ;l>>=1,siz<<=1) tr[l] = tr[l<<1]+tr[l<<1|1]+sum[l]*siz;
}
inline ll query(int l,int r){
ll res = 0;
int sizl = 0,sizr = 0,siz = 1;
for(l=l+P-1,r=r+P+1;l^1^r; ){
if(~l&1) res+=tr[l^1],sizl+=siz;
if(r&1) res+=tr[r^1],sizr+=siz;
l>>=1; r>>=1; siz<<=1;
res += sum[l]*sizl+sum[r]*sizr;
}
for(l>>=1,sizl+=sizr;l;l>>=1) res+=sum[l]*sizl;
return res;
}
int main(){
read(n,m);
while(P<=n+1) P<<=1;
for(int i=1;i<=n;++i) read(tr[P+i]);
for(int i=P-1;i;--i) tr[i] = tr[i<<1|1]+tr[i<<1];
int opt,l,r;ll k;
while(m--){
read(opt,l,r);
if(opt==1){
read(k);
change_add(l,r,k);
}
else{
printf("%lld\n",query(l,r));
}
}
return 0;
}
例二:Luogu P3373 【模板】线段树 2
注意下放标记时“加”和“乘”标记的下放顺序。
const int N = 1e5+10;
int n,m,P=1,DEP=0;
ll tr[N*3],sum[N*3],mul[N*3],mod;
inline void push_down(int p,ll siz){
siz >>= 1;
(tr[p<<1] = tr[p<<1]*mul[p]+sum[p]*siz)%=mod;
(sum[p<<1] = sum[p<<1]*mul[p]+sum[p])%=mod;
(mul[p<<1] *= mul[p])%=mod;
(tr[p<<1|1] = tr[p<<1|1]*mul[p]+sum[p]*siz)%=mod;
(sum[p<<1|1] = sum[p<<1|1]*mul[p]+sum[p])%=mod;
(mul[p<<1|1] *= mul[p])%=mod;
mul[p] = 1;sum[p] = 0;
}
inline void change_add(int l,int r,ll k){
l += P-1; r += P+1;
for(int i=DEP;i;--i) push_down(l>>i,1<<i),push_down(r>>i,1<<i);
int siz = 1;
while(l^1^r){
if(~l&1) (tr[l^1]+=k*siz)%=mod,(sum[l^1]+=k)%=mod;
if(r&1) (tr[r^1]+=k*siz)%=mod,(sum[r^1]+=k)%=mod;
l>>=1; r>>=1; siz<<=1;
(tr[l] = tr[l<<1]+tr[l<<1|1])%=mod;
(tr[r] = tr[r<<1]+tr[r<<1|1])%=mod;
}
for(l>>=1; l ;l>>=1) (tr[l] = tr[l<<1]+tr[l<<1|1])%=mod;
}
inline void change_mul(int l,int r,ll k){
l += P-1; r += P+1;
for(int i=DEP;i;--i) push_down(l>>i,1<<i),push_down(r>>i,1<<i);
while(l^1^r){
if(~l&1) (tr[l^1]*=k)%=mod,(mul[l^1]*=k)%=mod,(sum[l^1]*=k)%=mod;
if(r&1) (tr[r^1]*=k)%=mod,(mul[r^1]*=k)%=mod,(sum[r^1]*=k)%=mod;
l>>=1; r>>=1;
(tr[l] = tr[l<<1]+tr[l<<1|1])%=mod;
(tr[r] = tr[r<<1]+tr[r<<1|1])%=mod;
}
for(l>>=1; l ;l>>=1) (tr[l] = tr[l<<1]+tr[l<<1|1])%=mod;
}
inline ll query(int l,int r){
l += P-1; r += P+1;
for(int i=DEP;i;--i) push_down(l>>i,1<<i),push_down(r>>i,1<<i);
ll res = 0;
while(l^1^r){
if(~l&1) (res+=tr[l^1])%=mod;
if(r&1) (res+=tr[r^1])%=mod;
l>>=1; r>>=1;
}
return res%mod;
}
int main(){
read(n,m,mod);
while(P<=n+1) P<<=1,++DEP;
for(int i=1;i<=n;++i){
read(tr[P+i]); tr[P+i]%=mod;
mul[P+i] = 1;//sum[P+i] = 0;
}
for(int i=P-1;i;--i){
(tr[i] = tr[i<<1|1]+tr[i<<1])%=mod;
mul[i] = 1;//sum[i] = 0;
}
int opt,l,r;ll k;
while(m--){
read(opt,l,r);
if(opt==1){
read(k);
change_mul(l,r,k);
}
else if(opt==2){
read(k);
change_add(l,r,k);
}
else printf("%lld\n",query(l,r));
}
return 0;
}
例三:Luogu P3919 【模板】可持久化线段树 1(可持久化数组)
离散化后,输出别输出成离散化数据,记得输出原数。
const int N = 3e7+10,M = 1e6+10;
int P = 1,DEP = 0,NOW;
int tr[N],son[N][2],rt[M];
inline void update(int i,int vi,int val,int l){
int tl = l+P;
int v = rt[vi];
rt[i] = l = ++NOW;
for(int dep=DEP-1; dep>=0 ;--dep){
if((tl>>dep)&1) son[l][0] = son[v][0],son[l][1] = ++NOW,v = son[v][1];
else son[l][0] = ++NOW,son[l][1] = son[v][1],v = son[v][0];
l = NOW;
}
tr[l] = val;
}
inline int query(int i,int vi,int l){
rt[i] = rt[vi];
int tl = l+P;
l = rt[vi];
for(int dep=DEP-1; dep>=0 ;--dep){
if((tl>>dep)&1) l=son[l][1];
else l = son[l][0];
}
return tr[l];
}
int main(){
int n,T;read(n,T);
while(P<=n+1) P<<=1,++DEP; NOW = (1<<(DEP+1))-1;
for(int i=1;i<=n;++i) read(tr[P+i]); rt[0] = 1;
for(int i=P-1;i;--i) son[i][0]=i<<1,son[i][1]=i<<1|1;
for(int i=1,opt,vi,loc,val;i<=T;++i){
read(vi,opt,loc);
if(opt==1){
read(val);
update(i,vi,val,loc);
}
else printf("%d\n",query(i,vi,loc));
}
return 0;
}
例四:Luogu P3834 【模板】可持久化线段树 2
因为有人说 zkw 做不了主席树,我直接急了,所以必须得放一个主席树模板题证明自己。
主席树、划分树、归并树都可以做,给出的代码实现的是主席树。
注意先离散化数据,最后输出的时候输出原数据。
const int N = 2e7+10,M = 2e5+10;
int P = 1,DEP = 0,NOW;
int tr[N],son[N][2],rt[M],a[M],b[M],to[N];
inline void update(int i,int vi,int l,int k){
//新建节点,与可持久化线段树相同
int tl = l+P;
int v = rt[vi];
rt[i] = l = ++NOW;
for(int dep=DEP-1; dep>=0 ;--dep,l=NOW){
tr[l] = tr[v]+k;//更新当前节点信息
if((tl>>dep)&1) son[l][0] = son[v][0],son[l][1] = ++NOW,v = son[v][1];
else son[l][1] = son[v][1],son[l][0] = ++NOW,v = son[v][0];
}
tr[l] = tr[v]+k;//最后到叶子节点
to[l] = tl;//记录当前节点对应在初始树上的节点
}
inline int query(int l,int r,int k){
//查询第 k 大,与权值线段树相同
l = rt[l];r = rt[r];
for(int dep=0;dep<DEP;++dep){
//左右子树选举模拟二分过程
int num = tr[son[r][0]]-tr[son[l][0]];
if(num>=k){
l = son[l][0];
r = son[r][0];
}
else{
k -= num;
l = son[l][1];
r = son[r][1];
}
}
return to[l]-P;
}
int main(){
int n,T;read(n,T);
for(int i=1;i<=n;++i) read(a[i]),b[i]=a[i];
sort(b+1,b+1+n);
int idx = unique(b+1,b+1+n)-(b+1);
while(P<=idx+1) P<<=1,++DEP;
NOW = (1<<(DEP+1))-1; rt[0] = 1;//建初始线段树
for(int i=P-1;i;--i) son[i][0]=i<<1,son[i][1]=i<<1|1;
for(int i=1;i<=n;++i) to[i+P]=i+P;
for(int i=1,k;i<=n;++i){
k = lower_bound(b+1,b+idx+1,a[i])-b;
update(i,i-1,k,1);//插入离散化后的值
}
for(int i=1,l,r,k;i<=T;++i){
read(l,r,k);
printf("%d\n",b[query(l-1,r,k)]);
}
return 0;
}
例五:Luogu P3369 【模板】普通平衡树
本题有很多种解法,代码实现的是权值线段树。
const int N = 1e5+10;
int id[N],idx;
struct Query{
int opt,v;
}q[N];
int n;
int P = 1,DEP = 0,tr[N*3];
inline void update(int l,int k){
l += P;
tr[l] += k;
for(l>>=1; l ;l>>=1) tr[l] = tr[l<<1]+tr[l<<1|1];
}
inline int query(int l,int r){
l += P-1; r += P+1;
int res = 0;
while(l^1^r){
if(~l&1) res+=tr[l^1];
if(r&1) res+=tr[r^1];
l>>=1; r>>=1;
}
return res;
}
inline int get_rank(int k){
int dep = 0,l = 1;
while(dep<DEP){
if(k<=tr[l<<1]) l=l<<1;
else k-=tr[l<<1],l=l<<1|1;
++dep;
}
return l-P-1;
}
int main(){
read(n);
for(int i=1;i<=n;++i){
read(q[i].opt,q[i].v);
if(q[i].opt!=4) id[++idx] = q[i].v;
}
sort(id+1,id+idx+1);
idx = unique(id+1,id+idx+1)-(id+1);
while(P<=idx+1) P<<=1,++DEP;
for(int i=1,k,rk;i<=n;++i){
k = lower_bound(id+1,id+idx+1,q[i].v)-id;
if(q[i].opt==1) update(k,1);
else if(q[i].opt==2) update(k,-1);
else if(q[i].opt==3) printf("%d\n",(k==1) ? k : (query(1,k-1)+1));
else if(q[i].opt==4) printf("%d\n",id[get_rank(q[i].v)-1]);
else if(q[i].opt==5){
rk = query(1,k-1);
printf("%d\n",id[get_rank(rk)-1]);
}
else if(q[i].opt==6){
rk = query(1,k);
printf("%d\n",id[get_rank(rk+1)-1]);
}
}
return 0;
}
例六:Luogu P3380 【模板】树套树
本题经典写法是线段树套平衡树。给出代码实现的是树状数组套权值线段树。
线段树前缀和维护全局第 \(k\) 大,树状数组维护线段树的前缀和。每次用 \(lowbit\) 求出要修改和查询的线段树,然后整体修改查询即可。
注意求前驱和后继,与权值线段树的原理相同,结合 \(get\_rank\) 和 \(get\_num\) 即可。
const int N = 1e5+10,LG = 17,INF = 2147483647;
int n,m,a[N],b[N],idx;
int tmp[N][2],tmp0,tmp1;
struct Query{
int opt,l,r,k;
}q[N];
// #define ls(x) (x<<1)
// #define rs(x) (x<<1|1)
int P=1,DEP=0,NOW;
int tr[N*LG*LG],son[N*LG*LG][2],rt[N];//计算好空间
inline void update(int i,int l,int k){
int tl = l+P;
rt[i] ? 0 : rt[i]=++NOW;//动态开点
l = rt[i];
for(int dep=DEP-1;dep>=0;--dep){
tr[l] += k;
if((tl>>dep)&1) son[l][1]?0:son[l][1]=++NOW,l=son[l][1];
else son[l][0]?0:son[l][0]=++NOW,l=son[l][0];
}
tr[l] += k;
}
inline int query_rank(int l){
l += P;
int res = 0;
for(int dep=DEP-1;dep>=0;--dep){
if((l>>dep)&1){//前缀和
for(int i=1;i<=tmp0;++i) res-=tr[son[tmp[i][0]][0]],tmp[i][0]=son[tmp[i][0]][1];
for(int i=1;i<=tmp1;++i) res+=tr[son[tmp[i][1]][0]],tmp[i][1]=son[tmp[i][1]][1];
}
else{
for(int i=1;i<=tmp0;++i) tmp[i][0]=son[tmp[i][0]][0];
for(int i=1;i<=tmp1;++i) tmp[i][1]=son[tmp[i][1]][0];
}
}
return res;
}
inline int query_num(int k){
int l = 1;
for(int dep=0,res=0;dep<DEP;++dep,res=0){
for(int i=1;i<=tmp0;++i) res-=tr[son[tmp[i][0]][0]];
for(int i=1;i<=tmp1;++i) res+=tr[son[tmp[i][1]][0]];
if(k>res){//线段树上二分
k -= res;
for(int i=1;i<=tmp0;++i) tmp[i][0]=son[tmp[i][0]][1];
for(int i=1;i<=tmp1;++i) tmp[i][1]=son[tmp[i][1]][1];
l = l<<1|1;
}
else{
for(int i=1;i<=tmp0;++i) tmp[i][0]=son[tmp[i][0]][0];
for(int i=1;i<=tmp1;++i) tmp[i][1]=son[tmp[i][1]][0];
l = l<<1;
}
}
return l-P;
}
inline int get_rank(int l,int r,int k){
tmp0 = tmp1 = 0;
for(int i=l-1; i ;i-=(i&-i)) tmp[++tmp0][0] = rt[i];
for(int i=r; i ;i-=(i&-i)) tmp[++tmp1][1] = rt[i];
return query_rank(k)+1;
}
inline int get_num(int l,int r,int k){
tmp0 = tmp1 = 0;
for(int i=l-1; i ;i-=(i&-i)) tmp[++tmp0][0] = rt[i];
for(int i=r; i ;i-=(i&-i)) tmp[++tmp1][1] = rt[i];
return query_num(k);
}
inline int get_pre(int l,int r,int k){
int rk = get_rank(l,r,k)-1;
if(rk==0) return 0;//第一个数没前驱
return get_num(l,r,rk);
}
inline int get_nex(int l,int r,int k){
if(k>=idx) return idx+1;//最后一个数没后继
int rk = get_rank(l,r,k+1);
if(rk==r-l+2) return idx+1;//最后一个数
return get_num(l,r,rk);
}
// #define lowbit(x) (x&-x)
inline void add(int x,int k){
for(int i=x;i<=n;i+=(i&-i)) update(i,a[x],k);//求需要哪个线段树
}
int main(){
read(n,m);
for(int i=1;i<=n;++i){
read(a[i]);
b[++idx] = a[i];
}
for(int i=1;i<=m;++i){
read(q[i].opt);
if(q[i].opt==3) read(q[i].l,q[i].k);
else read(q[i].l,q[i].r,q[i].k);
if(q[i].opt==4 || q[i].opt==5 || q[i].opt==3) b[++idx] = q[i].k;
}
sort(b+1,b+1+idx);
idx = unique(b+1,b+1+idx)-(b+1);
while(P<=idx+1) P<<=1,++DEP;
for(int i=1;i<=n;++i){
a[i] = lower_bound(b+1,b+1+idx,a[i])-b;
add(i,1);
}
b[0] = -INF; b[idx+1] = INF;
for(int i=1;i<=m;++i){
if(q[i].opt!=2) q[i].k = lower_bound(b+1,b+1+idx,q[i].k)-b;
if(q[i].opt==1) printf("%d\n",get_rank(q[i].l,q[i].r,q[i].k));
else if(q[i].opt==2) printf("%d\n",b[get_num(q[i].l,q[i].r,q[i].k)]);
else if(q[i].opt==3){
add(q[i].l,-1);
a[q[i].l] = q[i].k;
add(q[i].l,1);
}
else if(q[i].opt==4) printf("%d\n",b[get_pre(q[i].l,q[i].r,q[i].k)]);
else if(q[i].opt==5) printf("%d\n",b[get_nex(q[i].l,q[i].r,q[i].k)]);
}
return 0;
}
例七:Luogu P3157 [CQOI2011] 动态逆序对
对答案有贡献的点对应满足:
- 删除时间 \(tim_i<tim_j\);
- 所在位置 \(pos_i<pos_j\);
- 数字权值 \(val_i>val_j\)。
此时题目就变成了三维偏序问题,经典的三维偏序离线做法是 CDQ 分治,在线做法可以用树套树实现。代码实现的是树状数组套线段树。
静态求逆序对时,我们可以使用树状数组。删除数据时,逆序对减少数量为前面的比当前数字大的数的个数以及后面的比当前数字小的数的个数。
我们先按删除时间排序消去第一维。为了维护数的权值和位置,我们再对每一个树状数组的节点建一棵权值线段树进行维护。
const int N = 1e5+10,LG = 18;
int n,m,a[N],idx[N],st[N];ll ans;
int tmp[N][2],tmp0,tmp1;
// #define ls(x) (x<<1)
// #define rs(x) (x<<1|1)
int P=1,DEP=0,NOW;
int tr[3*N+N*LG*LG],son[3*N+N*LG*LG][2],rt[N];
inline void update(int i,int l,int k){
int tl = l+P,stop = 0;
rt[i] ? 0 : rt[i]=++NOW;
l = rt[i]; st[++stop] = l;
for(int dep=DEP-1;dep>=0;--dep,st[++stop]=l){
if((tl>>dep)&1) son[l][1]?0:son[l][1]=++NOW,l=son[l][1];
else son[l][0]?0:son[l][0]=++NOW,l=son[l][0];
}
tr[l] += k;
while(--stop) tr[st[stop]] = tr[son[st[stop]][0]]+tr[son[st[stop]][1]];
}
inline ll query(int x,int y,int k,int f){
tmp0 = tmp1 = 0;
for(int i=x-1; i ;i-=(i&-i)) tmp[++tmp0][0]=rt[i];
for(int i=y; i ;i-=(i&-i)) tmp[++tmp1][1]=rt[i];
ll res = 0;
for(int dep=DEP-1,l=k+P;dep>=0;--dep){
if((l>>dep)&1){
if(f){
for(int i=1;i<=tmp0;++i) res-=tr[son[tmp[i][0]][0]];
for(int i=1;i<=tmp1;++i) res+=tr[son[tmp[i][1]][0]];
}
for(int i=1;i<=tmp0;++i) tmp[i][0]=son[tmp[i][0]][1];
for(int i=1;i<=tmp1;++i) tmp[i][1]=son[tmp[i][1]][1];
}
else{
if(!f){
for(int i=1;i<=tmp0;++i) res-=tr[son[tmp[i][0]][1]];
for(int i=1;i<=tmp1;++i) res+=tr[son[tmp[i][1]][1]];
}
for(int i=1;i<=tmp0;++i) tmp[i][0]=son[tmp[i][0]][0];
for(int i=1;i<=tmp1;++i) tmp[i][1]=son[tmp[i][1]][0];
}
}
return res;
}
// #define lowbit(x) (x&-x)
inline void add(int x,int k){
for(int i=x;i<=n;i+=(i&-i)) update(i,a[x],k);
}
int main(){
read(n,m);
while(P<=n+1) P<<=1,++DEP;
NOW = (1<<(DEP+1))-1;
for(int i=1;i<=n;++i){
read(a[i]);
idx[a[i]] = i;
ans += query(1,i-1,a[i],0);
add(i,1);
}
printf("%lld\n",ans);
for(int i=1,x;i<m;++i){
read(x);
ans -= query(1,idx[x]-1,x,0);
ans -= query(idx[x]+1,n,x,1);
printf("%lld\n",ans);
add(idx[x],-1);
}
return 0;
}
例八:Luogu P4198 楼房重建
兔队线段树,要求实现单点修改、动态维护最长上升子序列长度。本人题解。
根据生活经验,我们需要维护每栋楼房最高点到原点连线的斜率 \(k\) 的最长上升子序列。
显然地,对于一个区间,我们所选的起始楼房越靠左侧越优。所以在合并信息时,左区间答案始终不变,对右区间进行二分查找第一个大于左区间最大值的值。
所以我们需要记录每个区间的 \(k\) 的最大值,并且在查找右区间的过程中:
- 如果右区间最大值都比左区间最大值小了,那右区间对答案没有贡献;
- 如果右区间对答案有贡献,就继续向下查找;
- 当找到叶子节点时,看节点值是否大于左区间最大值。
先修改信息,然后在 zkw 线段树上动态模拟选点维护过程即可。
const int N = 1e5+10;
int n,m;
int P = 1,DEP = 0;
double mx[N*3];int len[N*3];
inline int query(int l,double k,int dep){
int tl = l;
int res = 0;
while(dep<DEP){
if(mx[l]<=k) break;//剪枝:右区间最大值比左区间的小
l = l<<1|(mx[l<<1]<=k);//模拟选取左右子树过程
dep++;
}
if(dep==DEP) res = (mx[l]>k);//叶子节点
while(l!=tl){
if(~l&1) l>>=1,res+=len[l]-len[l<<1];//累加答案
else l>>=1;
}
return res;
}
inline void update(int l,double k){
l += P;
//直接对叶子节点进行更新
len[l] = 1;mx[l] = k;
int dep = DEP;
//从下到上维护父子结点关系
for(l>>=1,--dep; l ;l>>=1,--dep){
mx[l] = max(mx[l<<1],mx[l<<1|1]);
len[l] = len[l<<1]+query(l<<1|1,mx[l<<1],dep+1);
}
}
int main(){
read(n,m);
while(P<=n+1) P<<=1,++DEP;
int l,r;
while(m--){
read(l,r);
update(l,(1.0*r)/(1.0*l));
printf("%d\n",len[1]);//输出根节点信息
}
return 0;
}