lxl 讲课记录
线段树和平衡树
P7706 文文的摄影布置
给定一个有 个二元组的数组 ,有三种操作,共 次。
,
,
,询问
简单题,我们用线段树维护,发现答案合并时就来自两个子树的答案和 , 共四种情况,我们维护一下 最大值即可。
我感觉可以修改可以推到区间上,因为内部相对大小不改变。
P6617 查找 Search
给定 ,有两种操作:
- 单点修改
- 询问区间是否能找出两个数使得其和为 。
一个很弱智的想法是对于每个数找到其对应的前一个点,然后判断是否区间全部点的对应点下标都小于 ,但是这样复杂度是错的,这个题的好处是它有一个支配性质:
满足条件且 ,则 相比 一定不优。
所以我们每个点只在 个点对里,此时复杂度正确。
而我们一个点点权的改变也只会造成 个点的对应点变化,然后就可以直接维护。
均摊复杂度
序列染色段数均摊
-
特点:修改有区间染色操作
-
用平衡树维护区间的颜色连续段
-
区间染色每次最多只会增加 个连续颜色段,用平衡树维护所有连续段即可
-
均摊的颜色段插入删除次数
- 应用:
- 区间染色,维护区间的复杂信息
- 区间排序
- “ODT”类问题
- 注意这里这个颜色段数均摊是有 的常数,常数很大
还有对于颜色段的区间修改:区间颜色段打标记一起动
-
区间染色类问题的复杂形式
-
一般会要求支持区间中每个颜色段进行移动,每个颜色段的移动是只需要考虑局部性质就可以确定的
-
设计一个可以合并的,移动颜色段的标记即可
-
需要注意颜色段长度变成 之后消失这种情况,处理时需要维护最短的连续段,当长度变成 时递归到叶子上找到这个颜色段并删除,同时修改局部的其他颜色段的局部相关信息
CF453E Little Pony and Lord Tirek
给一个序列,每个位置有初值 ,最大值 ,这个值每秒会增大 ,直到
有 个发生时间依此增大的询问,每次询问区间和并且将区间的所有数字变成
对于每一个数第一次被询问,我们显然可以暴力做。
我们对时间使用颜色段均摊,然后就相当于询问经过时间 后,每个位置会从 变成多少。
我们发现,对于 的元素,其贡献为 ,否则为 。
我们对 建立主席树,维护 和 ,即可解决这个问题。
P5066 [Ynoi2014] 人人本着正义之名
你需要帮珂朵莉维护一个长为 的 序列 ,有 个操作:
- :把区间 的数变成 。
- :把区间 的数变成 。
- : 内所有数 ,变为 与 按位或的值,这些数同时进行这个操作。
- : 内所有数 ,变为 与 按位或的值,这些数同时进行这个操作。
- : 内所有数 ,变为 与 按位与的值,这些数同时进行这个操作。
- : 内所有数 ,变为 与 按位与的值,这些数同时进行这个操作。
- :查询区间 的和。
本题强制在线,每次的 需要与上次答案做 运算,如果之前没有询问,则上次答案为 。
我们用平衡树维护若干个 的连续段,那么每个操作相当于让每个段的端点根据其颜色向左右移动 ,然后我们维护平衡树每颗子树里面有多少个 以及区间位置就能在平衡树上二分找到 分别是哪些颜色段。
然后我们要考虑一种区间长度变为 的情况,我们暴力回收这个段,由于只有 段,所以复杂度正确,为了发现是否有段长度变成了 ,我们还需要维护 段分别的长度最小值。
细节很多。
#include<bits/stdc++.h> using namespace std; const int N=4e6+5,INF=0x3f3f3f3f; mt19937 rng(time(0)); int ans,a[N],n,m; struct Node{ int tl[2],tr[2],cnt[2],mn[2],s,lc,rc,l,r,val,p; inline Node(){ tl[0]=tl[1]=tr[0]=tr[1]=cnt[0]=cnt[1]=s=lc=rc=l=r=val=p=0; mn[1]=mn[0]=INF; } }t[N]; int rt,tot; inline void up(int k){ if(!k) return; t[k].mn[0]=min(t[t[k].lc].mn[0],t[t[k].rc].mn[0]), t[k].mn[1]=min(t[t[k].lc].mn[1],t[t[k].rc].mn[1]), t[k].cnt[0]=t[t[k].lc].cnt[0]+t[t[k].rc].cnt[0], t[k].cnt[1]=t[t[k].lc].cnt[1]+t[t[k].rc].cnt[1], t[k].s=t[t[k].lc].s+t[t[k].rc].s+t[k].val*(t[k].r-t[k].l+1), t[k].mn[t[k].val]=min(t[k].r-t[k].l+1,t[k].mn[t[k].val]), t[k].cnt[t[k].val]++; } inline void add(int k,int l0,int r0,int l1,int r1){ if(!k) return; t[k].tl[0]+=l0,t[k].tl[1]+=l1,t[k].tr[0]+=r0,t[k].tr[1]+=r1; t[k].s+=t[k].cnt[1]*(r1-l1),t[k].mn[0]+=r0-l0,t[k].mn[1]+=r1-l1; if(!t[k].val) t[k].l+=l0,t[k].r+=r0; else t[k].l+=l1,t[k].r+=r1; } inline void push(int k){ assert(k); int &l0=t[k].tl[0],&r0=t[k].tr[0],&l1=t[k].tl[1],&r1=t[k].tr[1]; if(!l0&&!r0&&!l1&&!r1) return; add(t[k].lc,l0,r0,l1,r1),add(t[k].rc,l0,r0,l1,r1),l0=r0=l1=r1=0; } void split_l(int k,int lim,int &x,int &y){ if(!k) return x=y=0,void(); push(k); if(t[k].l<=lim) x=k,split_l(t[k].rc,lim,t[x].rc,y),up(x); else y=k,split_l(t[k].lc,lim,x,t[y].lc),up(y); } void split_r(int k,int lim,int &x,int &y){ if(!k) return x=y=0,void(); push(k); if(t[k].r<lim) x=k,split_r(t[k].rc,lim,t[x].rc,y),up(x); else y=k,split_r(t[k].lc,lim,x,t[y].lc),up(y); } int mer(int x,int y){ if(!x||!y) return x+y; push(x),push(y); if(t[x].p<t[y].p) return t[x].rc=mer(t[x].rc,y),up(x),x; else return t[y].lc=mer(x,t[y].lc),up(y),y; } inline int findl(int x){return push(x),t[x].lc?findl(t[x].lc):x;} inline int findr(int x){return push(x),t[x].rc?findr(t[x].rc):x;} inline int nd(int l,int r,int v){ if(l>r) return 0; t[++tot].p=rng(),t[tot].val=v,t[tot].l=l,t[tot].r=r; t[tot].s=(r-l+1)*v,t[tot].cnt[v]=1,t[tot].mn[v]=r-l+1; return tot; } inline void insert(int l,int r,int v){rt=mer(rt,nd(l,r,v));} inline void setval(int l,int r,int v){ int x,y,z,tx,ty; split_r(rt,l,x,y),split_l(y,r,y,z); tx=findl(y),ty=findr(y); if(t[tx].val==v) l=t[tx].l; else x=mer(x,nd(t[tx].l,l-1,t[tx].val)); if(t[ty].val==v) r=t[ty].r; else z=mer(nd(r+1,t[ty].r,t[ty].val),z); if(x){ tx=findr(x); if(t[tx].val==v) l=t[tx].l,split_r(x,t[tx].r,x,ty); } if(z){ ty=findl(z); if(t[ty].val==v) r=t[ty].r,split_l(z,t[ty].l,tx,z); } rt=mer(mer(x,nd(l,r,v)),z); } inline void ins(int l,int r,int l0,int r0,int l1,int r1,int v){ int x,y,z,tx,ty,tmp; split_r(rt,l,x,y),split_l(y,r,y,z); tx=findl(y),ty=findr(y); if(t[tx].val==v){ split_l(y,t[tx].l,tmp,y),x=mer(x,tmp); if(!y) return rt=mer(x,z),void(); } if(t[ty].val!=v){ split_r(y,t[ty].r,y,tmp),z=mer(tmp,z); if(!y) return rt=mer(x,z),void(); } add(y,l0,r0,l1,r1),rt=mer(mer(x,y),z); } inline bool ntr(int x){return min(t[x].mn[0],t[x].mn[1])==0;} void del(int k){ if(!k) return; push(k); if(t[k].l>t[k].r){ int x,y,z,tx,ty; if(t[k].l==1) return split_r(rt,1,tx,rt); if(t[k].r==n) return split_l(rt,n,rt,tx); split_r(rt,t[k].r,x,y),split_l(y,t[k].l,y,z); tx=findl(y),ty=findr(y); return rt=mer(mer(x,nd(t[tx].l,t[ty].r,t[tx].val)),z),void(); } if(ntr(t[k].lc)) return del(t[k].lc); if(ntr(t[k].rc)) return del(t[k].rc); } inline void maintain(){while(ntr(rt)) del(rt);} inline int ask(int l,int r){ int x,y,z,tx,ty; split_r(rt,l,x,y),split_l(y,r,y,z); tx=findl(y),ty=findr(y); int res=t[y].s-t[tx].val*(l-t[tx].l)-t[ty].val*(t[ty].r-r); rt=mer(mer(x,y),z); return res; } signed main(){ n=read(),m=read(); int lst=1,val=read(); for(int i=2;i<=n;++i) if(read()!=val) insert(lst,i-1,val),lst=i,val^=1; insert(lst,n,val); while(m--){ int opt=read(),l=read()^ans,r=read()^ans; if(opt<=2) setval(l,r,opt-1); if(opt==3) ins(l,r,0,-1,-1,0,1); if(opt==4) ins(l,r,1,0,0,1,0); if(opt==5) ins(l,r,-1,0,0,-1,0); if(opt==6) ins(l,r,0,1,1,0,1); if(opt==7) write(ans=ask(l,r)); maintain(); } flush(); }
PKUSC2021D1T2 逛街
lxl 锐评:所以这些比赛的出题人也没什么水平,出缝合题
我们同样考虑颜色段均摊,那么对于两端都大于两边的段,其每次操作长度增加 。
对于两端都小于的段,每次操作长度减小 。
其他情况长度不变。
然后同样维护这三种情况的数量,区间长度和,最小长度,如果出现长度为 就暴力回收,然后询问用类似于楼房重建的单侧递归方式即可。
P9061 Optimal Ordered Problem Solver
给定 个点 ,你需要按顺序处理 次操作。每次操作给出 ,
- 首先进行修改:
- 若 则将满足 的点的 修改为 ;
- 若 则将满足 的点的 修改为 。
- 然后进行查询,询问满足 的点数。
我们发现,被操作过的点构成了一条阶梯般的轮廓线,然后我们用平衡树维护这个轮廓线,由于这个轮廓线具有一个单调性,所以一次修改也是对这个平衡树的一段区间进行了 x 赋值或者 y 赋值,这个可以在平衡树上打标记维护。
修改时还需要我们找出新出现在轮廓线上的点,我们可以做一个扫描线,找到每个点第一次被覆盖的时间。
对于查询,我们做一个简单容斥,将询问分成三个部分减去全局总点数,需要在轮廓线上找出有贡献的点,这个可以在平衡树上二分。
对于还未被操作的点,是两个带修的 1−side 询问和一个未修改的 2−side 询问,二维数点即可。
这样我们就在 的时间复杂度内解决了问题。
被卡常了,mmsd
upd:加上两个优化过去了,一个是 插入时的优化,还有一个是代码中 findx,findy 时在树上二分而不是采用常数很大的分裂合并,跑得还很快。
#include<bits/stdc++.h> using namespace std; const int N=1e6+5; int n,m; mt19937 rng(time(0)); struct Node{ int x,y,p,tagx=-1,tagy=-1,tot,sz,lc,rc; }t[N]; int tot,rt; #define k1 t[k].lc #define k2 t[k].rc inline void push(int k){ if(~t[k].tagx){ if(k1) t[k1].tagx=t[k1].x=t[k].tagx; if(k2) t[k2].tagx=t[k2].x=t[k].tagx; t[k].tagx=-1; } if(~t[k].tagy){ if(k1) t[k1].tagy=t[k1].y=t[k].tagy; if(k2) t[k2].tagy=t[k2].y=t[k].tagy; t[k].tagy=-1; } } #define fir first #define sec second int mer(int x,int y){ if(!x||!y) return x+y; push(x),push(y); if(t[x].p<t[y].p) return t[x].rc=mer(t[x].rc,y),t[x].sz=t[t[x].lc].sz+t[t[x].rc].sz+1,x; else return t[y].lc=mer(x,t[y].lc),t[y].sz=t[t[y].lc].sz+t[t[y].rc].sz+1,y; } void split(int k,pair<int,int> V,int &x,int &y){ if(!k) return x=y=0,void(); push(k); if((t[k].x<V.fir)||(t[k].x==V.fir&&t[k].y>=V.sec)) x=k,split(t[k].rc,V,t[x].rc,y),t[x].sz=t[t[x].lc].sz+t[t[x].rc].sz+1; else y=k,split(t[k].lc,V,x,t[y].lc),t[y].sz=t[t[y].lc].sz+t[t[y].rc].sz+1; } void splitx(int k,int V,int &x,int &y){ if(!k) return x=y=0,void(); push(k); if(t[k].x<=V) x=k,splitx(t[k].rc,V,t[x].rc,y),t[x].sz=t[t[x].lc].sz+t[t[x].rc].sz+1; else y=k,splitx(t[k].lc,V,x,t[y].lc),t[y].sz=t[t[y].lc].sz+t[t[y].rc].sz+1; } void splity(int k,int V,int &x,int &y){ if(!k) return x=y=0,void(); push(k); if(t[k].y>V) x=k,splity(t[k].rc,V,t[x].rc,y),t[x].sz=t[t[x].lc].sz+t[t[x].rc].sz+1; else y=k,splity(t[k].lc,V,x,t[y].lc),t[y].sz=t[t[y].lc].sz+t[t[y].rc].sz+1; } int XX,YY,ZZ; void ins(int &now,int x,int y,int id){ if(!now) return now=id,void(); push(now); if(t[id].p<t[now].p) return split(now,{x,y},XX,YY),now=mer(XX,mer(tot,YY)),void(); if((t[now].x<x)||(t[now].x==x&&t[now].y>y)) ins(t[now].rc,x,y,id),t[now].sz=t[t[now].lc].sz+t[t[now].rc].sz+1; else ins(t[now].lc,x,y,id),t[now].sz=t[t[now].lc].sz+t[t[now].rc].sz+1; } inline void ins(int xx,int yy){ t[++tot].p=rng(),t[tot].x=xx,t[tot].y=yy,t[tot].sz=1,ins(rt,xx,yy,tot); } int findx(int k,int v){ if(!k) return 0; push(k); if(t[k].x<=v) return findx(k2,v)+t[k1].sz+1; else return findx(k1,v); } int findy(int k,int v){ if(!k) return 0; push(k); if(t[k].y<=v) return findy(k1,v)+t[k2].sz+1; else return findy(k2,v); } inline void change(int xx,int yy,int opt){ splitx(rt,xx,XX,YY),splity(XX,yy,XX,ZZ); if(!ZZ) return rt=mer(XX,YY),void(); if(opt==2) t[ZZ].tagx=t[ZZ].x=xx; else t[ZZ].tagy=t[ZZ].y=yy; rt=mer(mer(XX,ZZ),YY); } struct node{int x,y,id;}a[N],S1[N],S2[N],Q[N],tim[N]; inline bool cmp(node X,node Y){return X.x>Y.x;} inline bool cmp1(node X,node Y){return X.x<Y.x;} inline bool cmp2(node X,node Y){return X.id<Y.id;} namespace BIT1{ int c[N]; inline void add(int x,int v){for(;x;x-=x&-x) c[x]=min(c[x],v);} inline int ask(int x){ int res=m+1; for(;x<=n;x+=x&-x) res=min(res,c[x]); return res; } inline void init(){for(int i=1;i<=n;++i) c[i]=m+1;} } struct query{ int opt,a,b,c,d,id; }q[N]; int ans[N],Tim[N]; struct BIT{ int c[N]; inline void add(int t,int v){for(;t<=n;t+=t&-t) c[t]+=v;} inline int ask(int t){ int res=0; for(;t;t-=t&-t) res+=c[t]; return res; } inline int ask(int l,int r){return ask(r)-ask(l-1);} }X,Y,T; struct dat{int x,l,r,v,id;}G[N<<1]; inline bool CCmp(dat a,dat b){return a.x<b.x;} inline int downy(int xx){return Y.ask(xx)+findy(rt,xx);} inline int leftx(int xx){return X.ask(xx)+findx(rt,xx);} int qcnt; inline void add(int a,int b,int c,int d,int id){G[++qcnt]=(dat){a-1,b,d,-1,id},G[++qcnt]=(dat){c,b,d,1,id};} signed main(){ read(n,m); for(int i=1;i<=n;++i) read(a[i].x,a[i].y),a[i].id=i, X.add(a[i].x,1),Y.add(a[i].y,1); for(int i=1;i<=m;++i) read(q[i].opt,q[i].a,q[i].b,q[i].c,q[i].d),q[i].id=i, Q[i]={q[i].a,q[i].b,i},S1[i]={q[i].a-(q[i].opt==2),q[i].b-(q[i].opt==1),i},S2[i]={q[i].c,q[i].d,i}; BIT1::init(); sort(S1+1,S1+m+1,cmp),sort(S2+1,S2+m+1,cmp); sort(Q+1,Q+m+1,cmp),sort(a+1,a+n+1,cmp); int idx=1,Idx=1; for(int i=1;i<=m;++i){ while(idx<=n&&a[idx].x>Q[i].x) tim[idx]={a[idx].x,a[idx].y,BIT1::ask(a[idx].y)},++idx; BIT1::add(Q[i].y,Q[i].id); } while(idx<=n) tim[idx]={a[idx].x,a[idx].y,BIT1::ask(a[idx].y)},++idx; BIT1::init(); for(int i=1;i<=m;++i){ while(Idx<=m&&S2[Idx].x>S1[i].x) Tim[S2[Idx].id]=BIT1::ask(S2[Idx].y),++Idx; BIT1::add(S1[i].y,S1[i].id); } while(Idx<=m) Tim[S2[Idx].id]=BIT1::ask(S2[Idx].y),++Idx; sort(tim+1,tim+n+1,cmp2),idx=1; for(int i=1;i<=m;++i){ change(q[i].a,q[i].b,q[i].opt); while(idx<=n&&tim[idx].id==i){ if(q[i].opt==1) ins(tim[idx].x,q[i].b); else ins(q[i].a,tim[idx].y); X.add(tim[idx].x,-1),Y.add(tim[idx].y,-1),++idx; } if(Tim[i]>i) ans[i]=downy(q[i].d)+leftx(q[i].c)-n,add(q[i].c+1,q[i].d+1,n,n,i); } sort(a+1,a+n+1,cmp1),sort(G+1,G+qcnt+1,CCmp); idx=1,Idx=1; while(Idx<=qcnt&&G[Idx].x==0) ++Idx; for(int i=1;i<=n;++i){ while(idx<=n&&a[idx].x<=i) T.add(a[idx].y,1),++idx; while(Idx<=qcnt&&G[Idx].x==i) ans[G[Idx].id]+=G[Idx].v*T.ask(G[Idx].l,G[Idx].r),++Idx; } for(int i=1;i<=m;++i) println(ans[i]); }
普通均摊
CF679E Bear and Bad Powers of 42
定义一个正整数是坏的,当且仅当它是 的次幂,否则它是好的。
给定一个长度为 的序列 ,保证初始时所有数都是好的。
有 次操作,每次操作有三种可能:
- 查询 。
- 将 赋值为一个好的数 。
- 将 都加上 ,重复这一过程直到所有数都变好。
,。
我们考虑在可能的值域 内处理出所有 的次幂,这个数量很少。
先考虑操作三,我们维护每个点还差多少到达下一个 的次幂,等价于区间减,然后如果这个差的最小值小于 ,就暴力找到该点,更新成下一个还差的权值,然后最小值为 说明还需要操作,由于势能分析,这样的复杂度为 。
然后考虑加入二操作的情况,我们可以将每个颜色段同时维护,这样复杂度就正确了,每次操作只会带来 的势能。
这个可以平衡树无脑实现,也可以线段树在区间值全部相同的时候打标记实现。
CF702F T-Shirts
有 种 T 恤,每种有价格 和品质 。
有 个人要买 T 恤,第 个人有 元,每人每次都会买一件能买得起的 最大的 T 恤。一个人只能买一种 T 恤一件,所有人之间都是独立的。
问最后每个人买了多少件 T 恤?如果有多个 最大的 T 恤,会从价格低的开始买。
先考虑暴力,我们由于买 T 恤的顺序的固定的,所以我们可以将 T 恤排序,然后一个一个判断。
然后我们考虑用平衡树维护这个过程,用平衡树维护每个人,枚举 T 恤,然后我们发现大于 的数会集体减去 ,然后我们发现对于一个有 元钱的人,有三种情况。
- ,不用管。
- ,此时减去 后,与 的部分相对顺序会改变,我们暴力提取出来修改,然后扔回平衡树里即可。
- ,我们对 打上区间减标记,然后对 打上加 标记即可。
然后对于暴力的第二部分,每一次修改后 至少减少一半,每个点最多被暴力修改 次,所以复杂度正确。
总时间复杂度
扫描线
基础问题
CF1000F One Occurrence
给定长为 的序列, 次查询区间中有多少数只出现一次。
我们考虑每种数在什么情况下有贡献,我们发现,对于一种出现在 的数,这种数会对 的若干个矩形带来贡献,然后我们就将问题转化成了 次矩形加, 次单点查询,扫描线解决。
#637. A. 数据结构
给一个长为 的序列, 次查询:
如果将区间 中所有数都 ,那么整个序列有多少个不同的数?
询问间独立,也就是说每次查询后这个修改都会被撤销
出现不太好考虑,我们考虑一种颜色 在什么情况下没有贡献。
包含全部 ,且不包含任何一个 ,也就是对一些矩形的交有贡献,矩形的交还是矩形,还是可以扫描线做,时间复杂度 。
CF526F Pudding Monsters
给定一个 的棋盘,其中有 个棋子,每行每列恰好有一个棋子。
对于所有的 ,求有多少个 的子棋盘中恰好有 个棋子。
。
我们将根据棋子每个列对应一个行,那么我们就是要统计 的区间数量,对 扫描线,然后由于 ,所以我们统计最小值数量和最小值个数即可。
时间复杂度 。
区间子区间类模型
CF997E Good Subsegments
有一个的排列
如果区间中的数是连续的,那么我们称它为好区间。
有次询问,每次问内,有多少子区间是好的?
我们发现,这个题是上个题的加强版,我们考虑在上个问题的最小值个数上做历史版本和,具体来说,多两个数组,一个记录答案,一个标记记录要更新的次数,然后就可以了。
时间复杂度
CF103069G
给定一个序列,求区间有多少子区间,其内部出现过的颜色数为奇数
我们对 扫描线,发现操作等价于 序列区间异或,区间历史版本和,我们记录两个标记代表对 和 的答案贡献次数就可以了。
时间复杂度 。
换维扫描线
P3863序列
给定一个长度为 的序列,给出 个操作,形如:
表示将序列下标介于 的元素加上 (请注意, 可能为负)
表示查询 在过去的多少秒时间内不小于 (不包括这一秒,细节请参照样例)
开始时为第 秒,第 个操作发生在第 秒。
我们发现,如果将序列维和时间维看成二维平面,那么我们的修改就是矩形加,然后查询是单列查询,我们对下标扫描线,维护时间维,那么我们的操作等价于后缀加,并查询区间不小于 的数的个数,分块维护。
复杂度 。
P7560 [JOISC 2021 Day1] 饮食区
有一个长为 的序列,序列每个位置有个队列
有 个操作
- 每个操作形如 的每个队列中进来了 个 的人
或者 的每个队列中出去了 个人(不足 个则全部出去)
还有查询某个队列中第 个人的 (不足个输出 )
我们对下标扫描线,然后发现如果队列未清空过,那么我们查询第 个人可以先查询离开了的人数(比如 ),然后再在插入的人里二分找 人即可。
但是对于有前缀清空的时候,这种方法似乎就错误了。
但我们发现,清空的最后一次仅可能在前缀最小值时取到,于是我们维护前缀最小值和前缀最小值下标即可,注意判断没有清空过的情况。
// Problem: P7560 [JOISC 2021 Day1] フードコート // URL: https://www.luogu.com.cn/problem/P7560 // Memory Limit: 500 MB // Time Limit: 1000 ms // Author: Nityacke // Time: 2023-11-30 13:57:18 #include<bits/stdc++.h> #define int long long using namespace std; const int N=3e5+5; int n,m,q,col[N],Type[N],ans[N]; struct Node{int opt,x,t;}; vector<Node>vec[N]; struct node{ int sum,add,mn,pos; inline node(){sum=mn=add=pos=0;} }t[N<<2]; inline node operator +(node a,node b){ node c; c.mn=min(a.sum+b.mn,a.mn),c.sum=a.sum+b.sum, c.add=a.add+b.add,c.pos=(a.mn==c.mn?a.pos:b.pos); return c; } namespace ST{ #define k1 (k<<1) #define k2 (k<<1|1) #define mid ((l+r)>>1) inline void up(int k){t[k]=t[k1]+t[k2];} void build(int k=1,int l=1,int r=q){ if(l==r) return t[k].pos=l,void(); build(k1,l,mid),build(k2,mid+1,r),up(k); } void change(int x,int v,int type,int k=1,int l=1,int r=q){ if(l==r) return t[k].sum+=v,t[k].add+=v*type,t[k].mn=min(0ll,t[k].sum),void(); if(x<=mid) change(x,v,type,k1,l,mid); else change(x,v,type,k2,mid+1,r); up(k); } node ask(int x,int k=1,int l=1,int r=q){ if(x>=r) return t[k]; if(x<=mid) return ask(x,k1,l,mid); else return t[k1]+ask(x,k2,mid+1,r); } int query(int x,int &v,int k=1,int l=1,int r=q){ if(x<=l){ if(t[k].add<v) return v-=t[k].add,0; if(l==r) return l; if(t[k1].add>=v) return query(x,v,k1,l,mid); return query(x,v-=t[k1].add,k2,mid+1,r); } if(x<=mid){ int t=query(x,v,k1,l,mid); if(t) return t; } return query(x,v,k2,mid+1,r); } int asksum(int L,int R,int k=1,int l=1,int r=q){ if(L<=l&&R>=r) return t[k].add-t[k].sum; if(R<=mid) return asksum(L,R,k1,l,mid); if(L>mid) return asksum(L,R,k2,mid+1,r); return asksum(L,R,k1,l,mid)+asksum(L,R,k2,mid+1,r); } } signed main(){ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m>>q; int opt,x,y,z,t; for(int i=1;i<=q;++i){ cin>>opt>>x>>y; if(opt==1) cin>>z>>t,col[i]=z,vec[x].push_back((Node){1,i,t}),vec[y+1].push_back((Node){1,i,-t}); else if(opt==2) cin>>z,vec[x].push_back((Node){2,i,-z}),vec[y+1].push_back((Node){2,i,z}); else Type[i]=1,vec[x].push_back((Node){3,i,y}); } ST::build(); for(int i=1;i<=n;++i) for(auto v:vec[i]) if(v.opt<3) ST::change(v.x,v.t,2-v.opt); else{ node t=ST::ask(v.x); ans[v.x]=ST::query((t.mn<0)*t.pos+1,v.t+=ST::asksum((t.mn<0)*t.pos+1,v.x)); } for(int i=1;i<=q;++i) if(Type[i]) cout<<(ans[i]>i?0:col[ans[i]])<<endl; }
莫队
感觉例题都很唐,不想写。
分块
P6779 [Ynoi2009] rla1rmdq
给定一棵 个节点的树,树有非负边权,与一个长为 的序列 。
定义节点 的父亲为 ,根 满足 。
定义节点 的深度 为其到根简单路径上所有边权和。
有 次操作:
:对于 , 。
:查询对于 ,最小的 。
我们考虑分块,然后最小值具有支配性质,具体的,如果 这个树上节点之前到达过,那么这个往上跳的节点都没用了,然后我们均摊一下每个块只会访问树上每个节点一次,然后对于散快操作,暴力重剖往上跳 K 级祖先即可,由于每个点只会被跳 次,不影响复杂度,逐块处理可以做到线性空间。
P5063 [Ynoi2014] 置身天上之森
线段树是一种特殊的二叉树,满足以下性质:
每个点和一个区间对应,且有一个整数权值;
根节点对应的区间是 ;
如果一个点对应的区间是 ,且 ,那么它的左孩子和右孩子分别对应区间 和 ,其中 ;
如果一个点对应的区间是 ,且 ,那么这个点是叶子;
如果一个点不是叶子,那么它的权值等于左孩子和右孩子的权值之和。
珂朵莉需要维护一棵线段树,叶子的权值初始为 ,接下来会进行 次操作:
操作 :给出 ,对每个 (),将 对应的叶子的权值加上 ,非叶节点的权值相应变化;
操作 :给出 ,询问有多少个线段树上的点,满足这个点对应的区间被 包含,且权值小于等于 。
首先,我们发现线段树上只有 种节点大小,我们可以把节点大小相同的节点拉出来当一个序列,然后一次修改对每个序列的影响是区间加,然后边上的两块要特判一下,那么问题转化成了区间加区间小于等于某个数的个数,这个可以用分块轻松做到 的复杂度,然后有 个序列,看起来复杂度是 的,但仔细分析可以发现复杂度还是 ,并且空间复杂度是 ,如果会分散层叠可以做到 。
树套树和 CDQ 分治
[CQOI2011] 动态逆序对
典中典。
P3332 [ZJOI2013] K大数查询
你需要维护 个可重整数集,集合的编号从 到 。
这些集合初始都是空集,有 个操作:
- :表示将 加入到编号在 内的集合中
- :表示查询编号在 内的集合的并集中,第 大的数是多少。
注意可重集的并是不去除重复元素的,如 。
我们把权值线段树套外面,然后对于内层维护的线段树,我们转化成区间加问题,然后对于询问,我们在外层权值线段树上二分,然后再内层线段树上查询即可。
时间复杂度 。
P9068 [Ynoi Easy Round 2022] 超人机械 TEST_95
我们发现本质不同很困难,有一个经典升维然后带个 计算,但是这里还有个利用了支配性质的做法。
我们对每种权值维护第一次出现和最后一次出现位置,比如记 , 在 时刻变成了 第一次/最后一次出现的位置的贡献,那么贡献是:
至于删除贡献加一维就可以了,时间复杂度 ,空间 。
P4690 [Ynoi2016] 镜中的昆虫
区间染色,区间数颜色。
颜色段均摊简单题,修改直接把所有可能改变的点找出来暴力修改。
P3242 [HNOI2015] 接水果
给定树上 条带权路径,然后 次询问,每次给定一条路径 和一个数 ,询问被 包含的路径中,大小第 大的权值。
我们发现,那 条路径在二维平面上的的影响是 个矩形,那么问题转化成把一个点包括的矩形的第 大权值,扫描线一下发现是区间每个集合加入一个元素,询问某个集合第 大,树套树即可。
可持久化
P5795 [THUSC2015] 异或运算
给定长度为 的数列 和长度为 的数列 ,令矩阵 中第 行第 列的值 ,每次询问给定矩形区域 ,找出第 大的 。
。
我们发现 很小,我们可以设计一个 的做法。
对 中每个元素建出可持久化 Trie,然后每次询问拿 个数在可持久化 Trie 上二分即可。
CF464E The Classic Problem
给定一张 个点, 条边的无向图,每条边的边权为 ,求 到 的最短路,结果对 取模。
。
典题,我们用线段树维护每一个二进制位为多少,然后一次修改就是去变成 ,单点修改,主席树维护最短路即可。
P7561 [JOISC 2021 Day2] 道路の建設案 (Road Construction)
给出平面上 个点和一个数 ,请输出所有点对两两曼哈顿距离的前 小值。
我们考虑类似于超级钢琴的套路,那么问题转化成:每个点有一个平面,初始时此平面的点集就是所有点的集合,然后不断在此平面里查询距离这个点最近的点和删点。
对于询问距离,我们把贡献拆成 4 份,分开维护,对每个点维护左上,左下,右上,右下的最小贡献,这一部分用扫描线+主席树维护,然后删点新建一个版本即可。
时间复杂度 ,不过看起来常数巨大,还巨难写,我写了 3KB 后扔了。
但是还有一些很好写的做法,转切比雪夫距离,二分答案,然后按 排序,满足条件的点对在一个区间中,可以双指针维护,然后用 set 按 排序,维护这些点对,不难发现满足条件的点对也是一段区间,暴力统计点对,数量达到 就退出,这个做法的复杂度是 的,而且相当好写。
还有一个做法,转切比雪夫距离,二分答案,平面分块,发现能出现的点对在其周围的 个块中,具体的,加入每个点时,暴力统计周围块的点判断是否合法,点对数达到 就退出,然后发现这个查找的总不合法点对数量是 的,时间复杂度 ,常数在哈希表上。
树上问题
P1600 天天爱跑步
经典题,题意自己看吧。
我们发现,一条 的路径会对 造成贡献的条件是:
发现等价于子树中一个数出现次数,做法很多,可以线段树合并,转化成 子树前后桶大小拆分。
CF757G Can Bash Save the Day?
一棵 个点的树和一个排列 ,边有边权,支持两种操作:
- ,询问 。
- ,交换 。
,强制在线。
我们考虑对询问差分,然后这个 我们可以用 P4211 LCA 的套路,转化成链加链求和,对于强制在线,主席树即可。
然后我们发现对于询问,我们只会改变 版本的主席树,直接修改即可,时空复杂度 ,比较卡常,需要在操作进行一半的时候全部重构不然空间会炸,正解是 ,可持久化边分树。
你说得对,但是 lxl 说 lct 可以可持久化,不知道能不能做单 log
CF1017G The Tree
给定一棵树,维护以下3个操作:
表示如果节点 为白色,则将其染黑。否则对这个节点的所有儿子递归进行相同操作
表示将以节点 为根的子树染白。
表示查询节点 的颜色
我们发现,如果把一次 操作看成单点 ,所有点初值为 ,一个节点被染黑的条件是 的后缀最大值 ,然后对于 操作,一个很 trival 的想法是直接子树赋值成 ,但我们发现 的后缀最大值此时不一定为 ,我们要在 上减去这个最大值,也就是除去 的贡献,这样就是正确的。
LOJ6276
树,点有颜色,有多少链满足上面的颜色互不相同
每种颜色出现次数<=20,,4s
我们考虑找到哪些是不合法的,我们枚举同种颜色的两两点对,然后包含这个点对的路径在 dfn 序上是 个矩形,转化成矩形加,全局 个数,扫描线即可。
时间复杂度 。
树分治
一般维护路径信息的话点分治和链分治比较好写
维护子树信息的话链分治会很方便
边分治用来分析一些问题会比较方便,因为进行了点度数的分治
具体问题需要考虑具体使用哪种树分治会更简单,每种树分治有其优点和缺点
点分治
序列上的分治是每次找一个中点,然后统计经过中点的区间,然后递归下去计算
树的点分治每次找一个重心,然后统计经过重心的路径,然后把这个点删去,树变成了很多连通子图,递归下去计算
我们每次递归下去的连通子图大小至少减半,所以递归树的深度 ,所以点分治复杂度
点分治问题在于两两子树贡献的统计时,如果你直接做,可能会加上一个序列维的限制,就会很唐。
但是如果合并两个大小为 的子树复杂度为 的话,直接做复杂度又会假。
正确的方法是使用类似于哈夫曼树的方法,每次选出用堆最小的两个结构来合并,可以证明如果合并两个大小为 的子树复杂度为 的话,那么点分治的复杂度为 ,且优势在于我们只需要维护两个结构相互的贡献,在很多情况下可以代替边分治。
边分治
- 树的边分治每次找一条边,然后统计经过这条边的路径,然后把这条边删去,树变成了两个连通子图,递归下去计算
- 分治一般是想让分治出的每个子问题大小接近,所以我们尽可能让两边大小接近
- 实际上树的边分治更好地对应到了序列的分治
但是菊花图的时候复杂度会假,我们需要对树进行三度化,这样可以证明,每次子树大小最大只会是原来的 ,而且这个边分树会成为一颗二叉树,我们可以在上面做可持久化,边分树合并之类的神秘操作。
链分治
- 基于轻重链剖分的结构,可以看作是每次删去一条重链之后继续分治下去
- 实际上和树上启发式合并是等价的:我们观察启发式合并的时候,我们每次是把size小的子树插入size最大的子树,这个可以看做是这个size大的子树所在的重链被删除了,然后依次合并重链上的轻儿子到这个重儿子上
例题
P5314 [Ynoi2011] ODT
给你一棵树,边权为 ,有点权。
需要支持两个操作:
- :表示把树上 到 这条简单路径的所有点点权都加上 。
- :表示查询与点 距离小于等于 的所有点里面的第 小点权。
首先我们知道考虑链分治,一个节点最多有 个邻域的点不是重链头,而且我们知道一次链加只会访问 个重链头,然后我们就可以对每个结点维护一颗平衡树,维护其轻儿子的权值,修改时在重链头的父亲的平衡树中修改,查询时暴力插入自己,父亲,重儿子三个节点的权值即可,修改复杂度 ,查询复杂度 ,不平衡,我们可以对每个点维护其子树大小 大的儿子,则一次修改复杂度为 ,查询复杂度 ,由于复杂度平衡,所以 ,总时间复杂度 。
存在基于平衡树多树二分的 做法。
随便 YY 的题
给一棵树,点有点权,求所有路径中点权 xor 和最大的一条路径
Sol1:点分治
我们开一棵 Trie,维护插入的点到分治中心构成链的 xor 和,然后就是简单的查询和插入。
Sol2:边分治
需要三度化,优势是只需要合并两颗子树,虽然此题中无优越性。
Sol3:链分治
考虑启发式合并的过程,合并轻儿子前查询贡献,发现重儿子继承时会将 Trie 全局 xor 上一个值,我们可以懒惰处理,记录全局 xor 了 x,查询和插入时都 xor 上 x 即可。
CF150E Freezing with Style
给定一颗带边权的树,求一条边数在 之间的路径,并使得路径上边权的中位数最大。输出一条可行路径的两个端点。
典中典外面套个二分,然后点分治维护 为长度为 的权值最大值,发现两个结构合并复杂度是 ,所以按 排序,然后合并时单调队列做一下就行了。
时间复杂度 ,由于树形态不改变,为了卡常,可以提前记录点分治的形态,并将合并顺序记录下来。
P3292 [SCOI2016] 幸运数字
给出一棵 个点的树,有点权,然后有 次询问,每次询问给出 ,询问 和 上的点任意 xor 的最大值。
我们考虑点分治,发现只有 次线性基单点加入和 次线性基合并。
然后我们直接做就好了,时间复杂度 ,空间复杂度 。
有一个好写的前缀线性基做法。
LOJ6145
给出一棵 个点的树, 次询问一个点 到编号在 中的点的距离的最小值。
我们考虑点分治,每次用 到分治中心的权值 + 到分治中心的最小值更新答案,这个做法支持可持久化后做强制在线。
还有一个做法是将 扔到 个区间上,建虚树跑最短路。
可是虚树是十级内容,超纲了怎么办。
什么,这不是虚树,这是树上离散化,使用这个,复杂度变小变低。
所以我们只使用了离散化的知识
KDT
KDT 是最优正交范围搜索树,只有每次交换维度划分复杂度才是对的,且如果有插入,需要二进制分组,替罪羊式重构是错的。
二维范围修改查询问题:
1.对矩形中的元素进行一次修改
2.对矩形中的元素进行一次查询
修改对修改有结合律,修改对范围信息有分配律和结合律,范围信息对范围信息有交换律和结合律
有一个定理证明了在上面这个问题中,KDT是理论最优的范围修改查询数据结构,并且这个下界对离线情况依然成立
而二进制分组的查询复杂度是 ,而插入总复杂度是 ,十分的正确。
而且因为二进制分组带有时间顺序的信息,所以功能很完整。
注意:KDT 在查询信息不能剪枝时很慢,可以剪枝时更快。
KDT 在查询半平面,圆信息的时候全错,可以卡到飞起的。
Luogu3710 方方方的数据结构
第一行两个数 ,表示数列的长度和操作个数。
接下来 行每行 或 个数。
- 如果第一个数为 ,接下来跟三个数 ,表示把区间 中的数加上 。
- 如果第一个数为 ,接下来跟三个数 ,表示把区间 中的数乘上 。
- 如果第一个数为 ,接下来跟一个数 ,表示询问 位置的数 。
- 如果第一个数为 ,接下来跟一个数 ,表示将第 行输入的操作撤销(保证为加或者乘操作,一个操作不会被撤销两次)。
我们离线可以得到每个操作的时间范围,那么加上上序列维度,我们就是矩阵加和乘,然后我们发现矩阵有 个元素,单次操作 ,鉴定为废了。
但是我们发现,我们只有 个单点操作,那么我们的点数降到了 ,总时间复杂度 。
网上的神秘题
给一个长为 的序列 一个长为 的序列
是 序列
有 次操作,每次操作给出一个 ,将所有 的 异或 ,求有多少连续的 段
我们发现 连续段个数等价于有多少点两边各为 。
我们把每个点表示成平面上的 ,那么每次就是行,列做取反操作,然后求 的个数。
好像是 SOJ 上的题
我们把每个点看作二位平面上的 ,然后操作由于有均摊性,KDT 直接维护即可。
支配点对
- 初始有 n 个对象,两两对象之间可以产生一个贡献
- 每次给一个范围,求范围内的所有对象之间两两产生的贡献的信息合并
- 如果需要对两两对象计算贡献,并且每次询问需要求出范围内的两两对象的贡献,这样需要维护 对贡献,即 对二元组,复杂度过高
- 但是问题经常有性质,导致可以找出 对两两对象之间的贡献,只需要查询范围内这 对的贡献,即可覆盖 对的贡献
- 这个 对二元组就叫做原问题的 对二元组的一个支配集
- 找出支配集后一般问题会变成几种平凡形式
第一类支配对
- 两两对象产生一个贡献,总共产生 对贡献,但是这些贡献中本质不同的只有 种
- 常见的题型:树保留区间点
- 一般这种问题在树上是常见的,有的树上问题要范围内的点两两点求出 LCA,这样是求范围内两两对象的贡献,但是 LCA 只有本质不同的 个点
- 这种题的常见思路是做树上启发式合并,然后启发式合并时,加入点 时,只考虑编号为 的前驱后继的点和 产生二元组,这样会找出 个二元组,这些二元组可以支配原本 对二元组的贡献
P7880 [Ynoi2006] rldcot
给定一棵 个节点的树,树根为 ,每个点有一个编号,每条边有一个边权。
定义 表示一个点到根简单路径上边权的和, 表示 节点在树上的最近公共祖先。
共 组询问,每次询问给出 ,求对于所有点编号的二元组 满足 ,有多少种不同的 。
支配点对板子,我们考虑一个点 在哪些情况下会成为 lca
-
-
在 两颗不同子树中,存在两个点
然后我们就有一个做法,我们对每个点 找到存在于两颗不同子树的所有点对 ,然后对询问扫描线,同时用树状数组维护每种深度在哪个位置产生贡献,这样的点对有 个,此时复杂度为 。
然后我们发现,这 个点对有很多是完全不必要的,比如设 子树中分别有来自三颗不同子树的点 ,那么有可能产生贡献的点对只有 和 ,而 就不必要。
我们想想,发现只需要维护每个点在 其他子树的前驱后继,这一部分可以用树上启发式合并 + set 维护,此时点对数量也降到和树上启发式合并次数相同,为 ,总时间复杂度为
P8528 [Ynoi2003] 铃原露露
给定一棵有根树,顶点编号为 ,对 有 为 的父亲。 是 的排列。
共 次询问,每次询问给出 ,询问有多少个二元组 ,满足 ,且对任意 ,有 在树上的最近公共祖先 满足 。
对于所有不好做,我们考虑哪些区间是不满足条件的。
所以我们 dsu on tree 一下,寻找前驱后继,得到 个矩形,然后我们转化成矩形加,矩形 个数,扫描线后转化成历史 个数,直接做就好了。
时间复杂度
第二类支配对
- 两两对象产生一个贡献,总共产生 对贡献,但是这些贡献中本质不同的有 种
常见的题型:区间内两两算个东西然后求 或- 有两种常见的支配对形式:
- 第一种是对每个 i,可以用数据结构高效找出 个 ,这些 的贡献支配了所有 的贡献
- 第二种是对一维分治时,假设对大小 的问题进行分治,会产生 对跨过分治中线的贡献,这些贡献支配了本来两边产生的 对贡献,一般这种问题会对信息那一维分治,不对序列分治
- 第一种情况
- 的贡献可以支配 的贡献,则 构成的区间被 构成的区间包含,并且统计 的贡献后,统计 的贡献一定不改变询问答案
- 首先 的贡献一定是必要的,因为没有比 小的区间
对每个 ,我们先令 ,计算 的贡献
然后找到一个 ,满足 , 有贡献,当且仅当 的贡献无法支配 的贡献- 利用这一条性质对 的可行范围做限制,如果可以保证每次 可行范围减半,则对每个 最多有 个 有贡献
CF765F Souvenirs
我们发现,对于 ,如果 时, 这个点对就是没有贡献的,然后我们发现 ,有效的 点对只有 个,然后找出来扫描线即可。
NC262593 The Closest Pair
给定一个长度为 的序列 ,保证 互不相同
次询问一个区间 中选出两个数 , 的最小值。
我们将 转化成 ,然后我们不妨令贡献来自于 ,对于另一种情况可以翻转再做一遍。
我们扫描时枚举 ,然后由于所有数互不相同,我们枚举的 只有 对,且此时我们的 只能在一个值域区间内,然后我们先找出一个下标最大的 ,满足 ,然后再找出一个下标最大的 ,然后我们发现,如果 是一对有用的点对,那么就有 ,即 ,所以 ,有用的 只有 个,准确来说,所有有用点对数量最多为 对,我们对这些点对扫描线即可,时间复杂度 。
P9058 [Ynoi2004] rpmtdq
给定一棵有边权的无根树,需要回答一些询问。
定义 代表树上点 和点 之间的距离。
对于每一组询问,会给出 ,你需要输出 其中 。
。
我们考虑对树分治,然后我们发现,合并两个结构时,设一个结构中有 ,一个结构中有 ,到分治中心的距离分别为 ,且 ,那么如果 或者 ,那么 就是无意义的(因为被 支配),或者说,只有距离不大于 的 的前驱后继会出现贡献,这样我们在树上会找到 个点对,然后扫描线即可。
时间复杂度 。
本文作者:Nityacke
本文链接:https://www.cnblogs.com/Nityacke/p/lxltxdy.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步