lxl 讲课记录
线段树和平衡树
P7706 文文的摄影布置
给定一个有 \(n\) 个二元组的数组 \((a_i,b_i)\),有三种操作,共 \(m\) 次。
\(1\ x\ v\),\(a_x\gets v\)
\(2\ x\ v\),\(b_x\gets v\)
\(3\ l\ r\),询问 \(\max_{l\le i<i+1<j\le r} A_i+A_j-\min_{k=i+1}^{j-1}B_k\)
简单题,我们用线段树维护,发现答案合并时就来自两个子树的答案和 \(A,BA\),\(AB,A\) 共四种情况,我们维护一下 \(A,B,AB,BA\) 最大值即可。
我感觉可以修改可以推到区间上,因为内部相对大小不改变。
P6617 查找 Search
给定 \(n,m,w\),有两种操作:
- 单点修改
- 询问区间是否能找出两个数使得其和为 \(w\)。
\(1\le n,m,w\le 5\times 10^5,4s\)
一个很弱智的想法是对于每个数找到其对应的前一个点,然后判断是否区间全部点的对应点下标都小于 \(L\),但是这样复杂度是错的,这个题的好处是它有一个支配性质:
\(\forall (i,j),(l,r)\) 满足条件且 \(l\le i,j\le r\),则 \((l,r)\) 相比 \((i,j)\) 一定不优。
所以我们每个点只在 \(O(1)\) 个点对里,此时复杂度正确。
而我们一个点点权的改变也只会造成 \(O(1)\) 个点的对应点变化,然后就可以直接维护。
均摊复杂度
序列染色段数均摊
-
特点:修改有区间染色操作
-
用平衡树维护区间的颜色连续段
-
区间染色每次最多只会增加 \(O(1)\) 个连续颜色段,用平衡树维护所有连续段即可
-
均摊的颜色段插入删除次数 \(O(n+m)\)

- 应用:
- 区间染色,维护区间的复杂信息
- 区间排序
- “ODT”类问题
- 注意这里这个颜色段数均摊是有 \(2\sim 3\) 的常数,常数很大
还有对于颜色段的区间修改:区间颜色段打标记一起动
-
区间染色类问题的复杂形式
-
一般会要求支持区间中每个颜色段进行移动,每个颜色段的移动是只需要考虑局部性质就可以确定的
-
设计一个可以合并的,移动颜色段的标记即可
-
需要注意颜色段长度变成 \(0\) 之后消失这种情况,处理时需要维护最短的连续段,当长度变成 \(0\) 时递归到叶子上找到这个颜色段并删除,同时修改局部的其他颜色段的局部相关信息
CF453E Little Pony and Lord Tirek
给一个序列,每个位置有初值 \(a_i\),最大值 \(m_i\),这个值每秒会增大 \(r_i\),直到 \(m_i\)
有 \(m\) 个发生时间依此增大的询问,每次询问区间和并且将区间的所有数字变成 \(0\)
\(0\le n,m,a_i,m_i,r_i\le 10^5,0\le t_i\le 10^8\)
对于每一个数第一次被询问,我们显然可以暴力做。
我们对时间使用颜色段均摊,然后就相当于询问经过时间 \(T\) 后,每个位置会从 \(0\) 变成多少。
我们发现,对于 \(T\ge \lceil \frac {m_i} {r_i} \rceil\) 的元素,其贡献为 \(m_i\),否则为 \(Tr_i\)。
我们对 $ \lceil \frac {m_i} {r_i} \rceil$ 建立主席树,维护 \(\sum m_i\) 和 \(\sum r_i\),即可解决这个问题。
P5066 [Ynoi2014] 人人本着正义之名
你需要帮珂朵莉维护一个长为 \(n\) 的 \(01\) 序列 \(a\),有 \(m\) 个操作:
- \(1\ l\ r\):把区间 \([l,r]\) 的数变成 \(0\)。
- \(2\ l\ r\):把区间 \([l,r]\) 的数变成 \(1\)。
- \(3\ l\ r\):\([l,r-1]\) 内所有数 \(a_i\),变为 \(a_i\) 与 \(a_{i+1}\) 按位或的值,这些数同时进行这个操作。
- \(4\ l\ r\):\([l+1,r]\) 内所有数 \(a_i\),变为 \(a_i\) 与 \(a_{i-1}\) 按位或的值,这些数同时进行这个操作。
- \(5\ l\ r\):\([l,r-1]\) 内所有数 \(a_i\),变为 \(a_i\) 与 \(a_{i+1}\) 按位与的值,这些数同时进行这个操作。
- \(6\ l\ r\):\([l+1,r]\) 内所有数 \(a_i\),变为 \(a_i\) 与 \(a_{i-1}\) 按位与的值,这些数同时进行这个操作。
- \(7\ l\ r\):查询区间 \([l,r]\) 的和。
本题强制在线,每次的 \(l,r\) 需要与上次答案做 \(\operatorname{xor}\) 运算,如果之前没有询问,则上次答案为 \(0\)。
我们用平衡树维护若干个 \(01\) 的连续段,那么每个操作相当于让每个段的端点根据其颜色向左右移动 \(\pm 1\),然后我们维护平衡树每颗子树里面有多少个 \(01\) 以及区间位置就能在平衡树上二分找到 \([l,r]\) 分别是哪些颜色段。
然后我们要考虑一种区间长度变为 \(0\) 的情况,我们暴力回收这个段,由于只有 \(O(n+m)\) 段,所以复杂度正确,为了发现是否有段长度变成了 \(0\),我们还需要维护 \(01\) 段分别的长度最小值。
细节很多。
#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 锐评:所以这些比赛的出题人也没什么水平,出缝合题
我们同样考虑颜色段均摊,那么对于两端都大于两边的段,其每次操作长度增加 \(1\)。
对于两端都小于的段,每次操作长度减小 \(1\)。
其他情况长度不变。
然后同样维护这三种情况的数量,区间长度和,最小长度,如果出现长度为 \(0\) 就暴力回收,然后询问用类似于楼房重建的单侧递归方式即可。
P9061 Optimal Ordered Problem Solver
给定 \(n\) 个点 \((x_i,y_i)_{i=1}^n\),你需要按顺序处理 \(m\) 次操作。每次操作给出 \(o,x,y,X,Y\),
- 首先进行修改:
- 若 \(o=1\) 则将满足 \(x_i\le x,\;y_i\le y\) 的点的 \(y_i\) 修改为 \(y\);
- 若 \(o=2\) 则将满足 \(x_i\le x,\;y_i\le y\) 的点的 \(x_i\) 修改为 \(x\)。
- 然后进行查询,询问满足 \(x_i\le X,\;y_i\le Y\) 的点数。
\(1\le n,m\le 10^6,1\le x_i,y_i\le n\)
我们发现,被操作过的点构成了一条阶梯般的轮廓线,然后我们用平衡树维护这个轮廓线,由于这个轮廓线具有一个单调性,所以一次修改也是对这个平衡树的一段区间进行了 x 赋值或者 y 赋值,这个可以在平衡树上打标记维护。
修改时还需要我们找出新出现在轮廓线上的点,我们可以做一个扫描线,找到每个点第一次被覆盖的时间。
对于查询,我们做一个简单容斥,将询问分成三个部分减去全局总点数,需要在轮廓线上找出有贡献的点,这个可以在平衡树上二分。
对于还未被操作的点,是两个带修的 1−side 询问和一个未修改的 2−side 询问,二维数点即可。
这样我们就在 \(O((n+m)\log n)\) 的时间复杂度内解决了问题。
被卡常了,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
定义一个正整数是坏的,当且仅当它是 \(42\) 的次幂,否则它是好的。
给定一个长度为 \(n\) 的序列 \(a_i\),保证初始时所有数都是好的。
有 \(q\) 次操作,每次操作有三种可能:
- \(1\ i\) 查询 \(a_i\)。
- \(2\ l\ r\ x\) 将 \(a_{l\dots r}\) 赋值为一个好的数 \(x\)。
- \(3\ l\ r\ x\) 将 \(a_{l \dots r}\) 都加上 \(x\),重复这一过程直到所有数都变好。
\(n,q \le 10^5\),\(a_i,x \le 10^9\)。
我们考虑在可能的值域 \(O(qx)\) 内处理出所有 \(42\) 的次幂,这个数量很少。
先考虑操作三,我们维护每个点还差多少到达下一个 \(42\) 的次幂,等价于区间减,然后如果这个差的最小值小于 \(0\),就暴力找到该点,更新成下一个还差的权值,然后最小值为 \(0\) 说明还需要操作,由于势能分析,这样的复杂度为 \(O(n\log n\log_{42}V)\)。
然后考虑加入二操作的情况,我们可以将每个颜色段同时维护,这样复杂度就正确了,每次操作只会带来 \(O(\log n\log_{42}V)\) 的势能。
这个可以平衡树无脑实现,也可以线段树在区间值全部相同的时候打标记实现。
CF702F T-Shirts
有 \(n\) 种 T 恤,每种有价格 \(c_i\) 和品质 \(q_i\)。
有 \(m\) 个人要买 T 恤,第 \(i\) 个人有 \(v_i\) 元,每人每次都会买一件能买得起的 \(q_i\) 最大的 T 恤。一个人只能买一种 T 恤一件,所有人之间都是独立的。
问最后每个人买了多少件 T 恤?如果有多个 \(q_i\) 最大的 T 恤,会从价格低的开始买。
先考虑暴力,我们由于买 T 恤的顺序的固定的,所以我们可以将 T 恤排序,然后一个一个判断。
然后我们考虑用平衡树维护这个过程,用平衡树维护每个人,枚举 T 恤,然后我们发现大于 \(c\) 的数会集体减去 \(c\),然后我们发现对于一个有 \(v\) 元钱的人,有三种情况。
- \(v<c\),不用管。
- \(c\le v<2c\),此时减去 \(c\) 后,与 \(v<c\) 的部分相对顺序会改变,我们暴力提取出来修改,然后扔回平衡树里即可。
- \(v\ge 2c\),我们对 \(v\) 打上区间减标记,然后对 \(ans\) 打上加 \(1\) 标记即可。
然后对于暴力的第二部分,每一次修改后 \(v\) 至少减少一半,每个点最多被暴力修改 \(O(\log v)\) 次,所以复杂度正确。
总时间复杂度 \(O(n\log n\log v+m\log n)\)
扫描线
基础问题
CF1000F One Occurrence
给定长为 \(n\) 的序列,\(m\) 次查询区间中有多少数只出现一次。
\(1\le n,m\le 5\times 10^5\)
我们考虑每种数在什么情况下有贡献,我们发现,对于一种出现在 \(a_1,a_2,\ldots a_k\) 的数,这种数会对 \(l\in [a_{i-1}+1,a_{i}],r\in[a_{i},a_{i+1}-1]\) 的若干个矩形带来贡献,然后我们就将问题转化成了 \(O(n)\) 次矩形加,\(O(m)\) 次单点查询,扫描线解决。
#637. A. 数据结构
给一个长为 \(n\) 的序列,\(m\) 次查询:
如果将区间 \([l,r]\) 中所有数都 \(+1\),那么整个序列有多少个不同的数?
询问间独立,也就是说每次查询后这个修改都会被撤销
\(1\le n,m\le 10^6\)
出现不太好考虑,我们考虑一种颜色 \(x\) 在什么情况下没有贡献。
包含全部 \(x\),且不包含任何一个 \(x-1\),也就是对一些矩形的交有贡献,矩形的交还是矩形,还是可以扫描线做,时间复杂度 \(O((n+m)\log n)\)。
CF526F Pudding Monsters
给定一个 \(n \times n\) 的棋盘,其中有 \(n\) 个棋子,每行每列恰好有一个棋子。
对于所有的 \(1 \leq k \leq n\),求有多少个 \(k \times k\) 的子棋盘中恰好有 \(k\) 个棋子。
\(n \le 3 \times 10^5\)。
我们将根据棋子每个列对应一个行,那么我们就是要统计 \(\max-\min-len=-1\) 的区间数量,对 \(r\) 扫描线,然后由于 \(\max-\min-len\ge -1\),所以我们统计最小值数量和最小值个数即可。
时间复杂度 \(O(n\log n)\)。
区间子区间类模型
CF997E Good Subsegments
有一个\(1-n\)的排列 \(P\)
如果区间\([l,r]\)中的数是连续的,那么我们称它为好区间。
有\(q\)次询问,每次问\([l,r]\)内,有多少子区间是好的?
\(1\le n,q\le 1.2\times 10^5\)
我们发现,这个题是上个题的加强版,我们考虑在上个问题的最小值个数上做历史版本和,具体来说,多两个数组,一个记录答案,一个标记记录要更新的次数,然后就可以了。
时间复杂度 \(O((n+q)\log n)\)
CF103069G
给定一个序列,求区间有多少子区间,其内部出现过的颜色数为奇数
我们对 \(r\) 扫描线,发现操作等价于 \(01\) 序列区间异或,区间历史版本和,我们记录两个标记代表对 \(1\) 和 \(0\) 的答案贡献次数就可以了。
时间复杂度 \(O((n+m)\log n)\)。
换维扫描线
P3863序列
给定一个长度为 \(n\) 的序列,给出 \(q\) 个操作,形如:
\(1~l~r~x\) 表示将序列下标介于 \([l,r]\) 的元素加上 \(x\) (请注意,\(x\) 可能为负)
\(2~p~y\) 表示查询 \(a_p\) 在过去的多少秒时间内不小于 \(y\) (不包括这一秒,细节请参照样例)
开始时为第 \(0\) 秒,第 \(i\) 个操作发生在第 \(i\) 秒。
我们发现,如果将序列维和时间维看成二维平面,那么我们的修改就是矩形加,然后查询是单列查询,我们对下标扫描线,维护时间维,那么我们的操作等价于后缀加,并查询区间不小于 \(y\) 的数的个数,分块维护。
复杂度 \(O(n\sqrt {n\log n})\)。
P7560 [JOISC 2021 Day1] 饮食区
有一个长为 \(n\) 的序列,序列每个位置有个队列
有 \(m\) 个操作
- 每个操作形如 \([l,r]\) 的每个队列中进来了 \(k\) 个 \(type=c\) 的人
或者 \([l,r]\) 的每个队列中出去了 \(k\) 个人(不足 \(k\) 个则全部出去)
还有查询某个队列中第 \(k\) 个人的 \(type\)(不足\(k\)个输出 \(0\))
\(1\le n.,m\le 2.5\times10^5\)
我们对下标扫描线,然后发现如果队列未清空过,那么我们查询第 \(k\) 个人可以先查询离开了的人数(比如 \(x\) ),然后再在插入的人里二分找 \(k+x\) 人即可。
但是对于有前缀清空的时候,这种方法似乎就错误了。
但我们发现,清空的最后一次仅可能在前缀最小值时取到,于是我们维护前缀最小值和前缀最小值下标即可,注意判断没有清空过的情况。
// 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
给定一棵 \(n\) 个节点的树,树有非负边权,与一个长为 \(n\) 的序列 \(a\)。
定义节点 \(x\) 的父亲为 \(fa(x)\),根 \(rt\) 满足 \(fa(rt)=rt\)。
定义节点 \(x\) 的深度 \(dep(x)\) 为其到根简单路径上所有边权和。
有 \(m\) 次操作:
\(1\ l\ r\):对于 \(l \le i \le r\), \(a_i := fa(a_i)\) 。
\(2\ l\ r\) :查询对于 \(l \le i \le r\),最小的 \(dep(a_i)\)。
\(1\le n,m\le 2\times 10^5,3s,64MB\)
我们考虑分块,然后最小值具有支配性质,具体的,如果 \(x\) 这个树上节点之前到达过,那么这个往上跳的节点都没用了,然后我们均摊一下每个块只会访问树上每个节点一次,然后对于散快操作,暴力重剖往上跳 K 级祖先即可,由于每个点只会被跳 \(O(\log n)\) 次,不影响复杂度,逐块处理可以做到线性空间。
P5063 [Ynoi2014] 置身天上之森
线段树是一种特殊的二叉树,满足以下性质:
每个点和一个区间对应,且有一个整数权值;
根节点对应的区间是 \([1,n]\);
如果一个点对应的区间是 \([l,r]\),且 \(l<r\),那么它的左孩子和右孩子分别对应区间 \([l,m]\) 和 \([m+1,r]\),其中 \(m=\lfloor\frac{l+r}{2}\rfloor\);
如果一个点对应的区间是 \([l,r]\),且 \(l=r\),那么这个点是叶子;
如果一个点不是叶子,那么它的权值等于左孩子和右孩子的权值之和。
珂朵莉需要维护一棵线段树,叶子的权值初始为 \(0\),接下来会进行 \(m\) 次操作:
操作 \(1\):给出 \(l,r,a\),对每个 \(x\)(\(l\leq x\leq r\)),将 \([x,x]\) 对应的叶子的权值加上 \(a\),非叶节点的权值相应变化;
操作 \(2\):给出 \(l,r,a\),询问有多少个线段树上的点,满足这个点对应的区间被 \([l,r]\) 包含,且权值小于等于 \(a\)。\(1\le n,m\le 10^5\)
首先,我们发现线段树上只有 \(O(\log n)\) 种节点大小,我们可以把节点大小相同的节点拉出来当一个序列,然后一次修改对每个序列的影响是区间加,然后边上的两块要特判一下,那么问题转化成了区间加区间小于等于某个数的个数,这个可以用分块轻松做到 \(O((n+m)\sqrt {n\log n})\) 的复杂度,然后有 \(O(\log n)\) 个序列,看起来复杂度是 \(O((n+m)\sqrt {n\log n}\log n)\) 的,但仔细分析可以发现复杂度还是 \(O((n+m)(\sqrt {n\log n}+\sqrt {\frac n 2 \log {\frac n 2}}+\sqrt {\frac n 4\log \frac n 4}+\ldots))=O((n+m)\sqrt {n\log n})\),并且空间复杂度是 \(O(n+m)\),如果会分散层叠可以做到 \(O((n+m)\sqrt n)\)。
树套树和 CDQ 分治
[CQOI2011] 动态逆序对
典中典。
P3332 [ZJOI2013] K大数查询
你需要维护 \(n\) 个可重整数集,集合的编号从 \(1\) 到 \(n\)。
这些集合初始都是空集,有 \(m\) 个操作:
- \(1\ l\ r\ c\):表示将 \(c\) 加入到编号在 \([l,r]\) 内的集合中
- \(2\ l\ r\ c\):表示查询编号在 \([l,r]\) 内的集合的并集中,第 \(c\) 大的数是多少。
注意可重集的并是不去除重复元素的,如 \(\{1,1,4\}\cup\{5,1,4\}=\{1,1,4,5,1,4\}\)。
我们把权值线段树套外面,然后对于内层维护的线段树,我们转化成区间加问题,然后对于询问,我们在外层权值线段树上二分,然后再内层线段树上查询即可。
时间复杂度 \(O(m\log^2 n)\)。
P9068 [Ynoi Easy Round 2022] 超人机械 TEST_95
我们发现本质不同很困难,有一个经典升维然后带个 \(pre_i\) 计算,但是这里还有个利用了支配性质的做法。
我们对每种权值维护第一次出现和最后一次出现位置,比如记 \(w(x,y,0/1,t)\),\(x\) 在 \(t\) 时刻变成了 \(y\) 第一次/最后一次出现的位置的贡献,那么贡献是:
至于删除贡献加一维就可以了,时间复杂度 \(O(n\log^2 n)\),空间 \(O(n)\)。
P4690 [Ynoi2016] 镜中的昆虫
区间染色,区间数颜色。
颜色段均摊简单题,修改直接把所有可能改变的点找出来暴力修改。
P3242 [HNOI2015] 接水果
给定树上 \(p\) 条带权路径,然后 \(m\) 次询问,每次给定一条路径 \(S\) 和一个数 \(k\),询问被 \(S\) 包含的路径中,大小第 \(k\) 大的权值。
我们发现,那 \(p\) 条路径在二维平面上的的影响是 \(O(p)\) 个矩形,那么问题转化成把一个点包括的矩形的第 \(K\) 大权值,扫描线一下发现是区间每个集合加入一个元素,询问某个集合第 \(k\) 大,树套树即可。
可持久化
P5795 [THUSC2015] 异或运算
给定长度为 \(n\) 的数列 \(X={x_1,x_2,...,x_n}\) 和长度为 \(m\) 的数列 \(Y={y_1,y_2,...,y_m}\),令矩阵 \(A\) 中第 \(i\) 行第 \(j\) 列的值 \(A_{i,j}=x_i\ \operatorname{xor}\ y_j\),每次询问给定矩形区域 \(i∈[u,d],j∈[l,r]\),找出第 \(k\) 大的 \(A_{i,j}\)。
\(1\leq n\leq 1000,1\leq m\leq 300000,1\le p\le 500\)。
我们发现 \(n,p\) 很小,我们可以设计一个 \(O(np \text{polylog}(m))\) 的做法。
对 \(Y\) 中每个元素建出可持久化 Trie,然后每次询问拿 \(O(n)\) 个数在可持久化 Trie 上二分即可。
CF464E The Classic Problem
给定一张 \(n\) 个点,\(m\) 条边的无向图,每条边的边权为 \(2^{x_i}\),求 \(s\) 到 \(t\) 的最短路,结果对 \(10^9+7\) 取模。
\(n, m, x \leq 10^5\)。
典题,我们用线段树维护每一个二进制位为多少,然后一次修改就是去变成 \(0\),单点修改,主席树维护最短路即可。
P7561 [JOISC 2021 Day2] 道路の建設案 (Road Construction)
给出平面上 \(n\) 个点和一个数 \(k\),请输出所有点对两两曼哈顿距离的前 \(k\) 小值。
\(1\le n,k\le 2.5\times 10^5\)
我们考虑类似于超级钢琴的套路,那么问题转化成:每个点有一个平面,初始时此平面的点集就是所有点的集合,然后不断在此平面里查询距离这个点最近的点和删点。
对于询问距离,我们把贡献拆成 4 份,分开维护,对每个点维护左上,左下,右上,右下的最小贡献,这一部分用扫描线+主席树维护,然后删点新建一个版本即可。
时间复杂度 \(O((n+k)\log n)\),不过看起来常数巨大,还巨难写,我写了 3KB 后扔了。
但是还有一些很好写的做法,转切比雪夫距离,二分答案,然后按 \(X\) 排序,满足条件的点对在一个区间中,可以双指针维护,然后用 set 按 \(Y\) 排序,维护这些点对,不难发现满足条件的点对也是一段区间,暴力统计点对,数量达到 \(k\) 就退出,这个做法的复杂度是 \(O((n+k)\log n\log V)\) 的,而且相当好写。
还有一个做法,转切比雪夫距离,二分答案,平面分块,发现能出现的点对在其周围的 \(O(1)\) 个块中,具体的,加入每个点时,暴力统计周围块的点判断是否合法,点对数达到 \(k\) 就退出,然后发现这个查找的总不合法点对数量是 \(O(n+k)\) 的,时间复杂度 \(O((n+k)\log V)\),常数在哈希表上。
树上问题
P1600 天天爱跑步
经典题,题意自己看吧。
我们发现,一条 \(u\to v\) 的路径会对 \(x\) 造成贡献的条件是:
- \(\sum_{u\in subtree_u}[d_x-a_x=d_u]\)
- \(\sum_{v\in subtree_x}[d_x-a_x=2d_{lca}-d_u]\)
发现等价于子树中一个数出现次数,做法很多,可以线段树合并,转化成 \(dfs\) 子树前后桶大小拆分。
CF757G Can Bash Save the Day?
一棵 \(n\) 个点的树和一个排列 \(p_i\),边有边权,支持两种操作:
- \(l\ r\ x\),询问 \(\sum\limits _{i=l}^{r} dis(p_i,x)\)。
- \(x\),交换 \(p_x,p_{x+1}\)。
\(n,q\leq 2\times 10^5\),强制在线。
我们考虑对询问差分,然后这个 \(dis\) 我们可以用 P4211 LCA 的套路,转化成链加链求和,对于强制在线,主席树即可。
然后我们发现对于询问,我们只会改变 \(x\) 版本的主席树,直接修改即可,时空复杂度 \(O((n+m)\log^2 n)\),比较卡常,需要在操作进行一半的时候全部重构不然空间会炸,正解是 \(O((n+m)\log n)\),可持久化边分树。
你说得对,但是 lxl 说 lct 可以可持久化,不知道能不能做单 log
CF1017G The Tree
给定一棵树,维护以下3个操作:
\(1\ x\)表示如果节点 \(x\) 为白色,则将其染黑。否则对这个节点的所有儿子递归进行相同操作
\(2\ x\)表示将以节点 \(x\) 为根的子树染白。
\(3\ x\)表示查询节点 \(x\) 的颜色
我们发现,如果把一次 \(1\) 操作看成单点 \(+1\),所有点初值为 \(-1\),一个节点被染黑的条件是 \(x\to rt\) 的后缀最大值 \(\ge 0\),然后对于 \(2\) 操作,一个很 trival 的想法是直接子树赋值成 \(-1\),但我们发现 \(x\to rt\) 的后缀最大值此时不一定为 \(-1\),我们要在 \(x\) 上减去这个最大值,也就是除去 \(f_x\to rt\) 的贡献,这样就是正确的。
LOJ6276
树,点有颜色,有多少链满足上面的颜色互不相同
每种颜色出现次数<=20,\(n\le1e5\),4s
我们考虑找到哪些是不合法的,我们枚举同种颜色的两两点对,然后包含这个点对的路径在 dfn 序上是 \(O(1)\) 个矩形,转化成矩形加,全局 \(0\) 个数,扫描线即可。
时间复杂度 \(O(cn\log n)\)。
树分治
一般维护路径信息的话点分治和链分治比较好写
维护子树信息的话链分治会很方便
边分治用来分析一些问题会比较方便,因为进行了点度数的分治
具体问题需要考虑具体使用哪种树分治会更简单,每种树分治有其优点和缺点
点分治
序列上的分治是每次找一个中点,然后统计经过中点的区间,然后递归下去计算
树的点分治每次找一个重心,然后统计经过重心的路径,然后把这个点删去,树变成了很多连通子图,递归下去计算
我们每次递归下去的连通子图大小至少减半,所以递归树的深度 \(O( \log n )\),所以点分治复杂度 \(O( n\log n )\)
点分治问题在于两两子树贡献的统计时,如果你直接做,可能会加上一个序列维的限制,就会很唐。
但是如果合并两个大小为 \(O(a),O(b)\) 的子树复杂度为 \(O(a+b)\) 的话,直接做复杂度又会假。
正确的方法是使用类似于哈夫曼树的方法,每次选出用堆最小的两个结构来合并,可以证明如果合并两个大小为 \(O(a),O(b)\) 的子树复杂度为 \(O(a+b)\) 的话,那么点分治的复杂度为 \(O(n\log n)\),且优势在于我们只需要维护两个结构相互的贡献,在很多情况下可以代替边分治。
边分治
- 树的边分治每次找一条边,然后统计经过这条边的路径,然后把这条边删去,树变成了两个连通子图,递归下去计算
- 分治一般是想让分治出的每个子问题大小接近,所以我们尽可能让两边大小接近
- 实际上树的边分治更好地对应到了序列的分治
但是菊花图的时候复杂度会假,我们需要对树进行三度化,这样可以证明,每次子树大小最大只会是原来的 \(\frac 2 3\),而且这个边分树会成为一颗二叉树,我们可以在上面做可持久化,边分树合并之类的神秘操作。
链分治
- 基于轻重链剖分的结构,可以看作是每次删去一条重链之后继续分治下去
- 实际上和树上启发式合并是等价的:我们观察启发式合并的时候,我们每次是把size小的子树插入size最大的子树,这个可以看做是这个size大的子树所在的重链被删除了,然后依次合并重链上的轻儿子到这个重儿子上
例题
P5314 [Ynoi2011] ODT
给你一棵树,边权为 \(1\),有点权。
需要支持两个操作:
- \(1\ x\ y\ z\):表示把树上 \(x\) 到 \(y\) 这条简单路径的所有点点权都加上 \(z\)。
- \(2\ x\ y\):表示查询与点 \(x\) 距离小于等于 \(1\) 的所有点里面的第 \(y\) 小点权。
首先我们知道考虑链分治,一个节点最多有 \(O(1)\) 个邻域的点不是重链头,而且我们知道一次链加只会访问 \(O(1)\) 个重链头,然后我们就可以对每个结点维护一颗平衡树,维护其轻儿子的权值,修改时在重链头的父亲的平衡树中修改,查询时暴力插入自己,父亲,重儿子三个节点的权值即可,修改复杂度 \(O(\log^2n)\),查询复杂度 \(O(\log n)\),不平衡,我们可以对每个点维护其子树大小 \(O(a)\) 大的儿子,则一次修改复杂度为 \(O(\log_a n\log n)\),查询复杂度 \(O(a\log n)\),由于复杂度平衡,所以 \(O(\log_a n)=O(a),a=O(\frac {\log n}{\log \log n})\),总时间复杂度 \(O(n\log n+m\frac {\log^2 n}{\log \log n})\)。
存在基于平衡树多树二分的 \(O((n+m)\log n)\) 做法。
随便 YY 的题
给一棵树,点有点权,求所有路径中点权 xor 和最大的一条路径
Sol1:点分治
我们开一棵 Trie,维护插入的点到分治中心构成链的 xor 和,然后就是简单的查询和插入。
Sol2:边分治
需要三度化,优势是只需要合并两颗子树,虽然此题中无优越性。
Sol3:链分治
考虑启发式合并的过程,合并轻儿子前查询贡献,发现重儿子继承时会将 Trie 全局 xor 上一个值,我们可以懒惰处理,记录全局 xor 了 x,查询和插入时都 xor 上 x 即可。
CF150E Freezing with Style
给定一颗带边权的树,求一条边数在 \([L,R]\) 之间的路径,并使得路径上边权的中位数最大。输出一条可行路径的两个端点。
\(1\le n\le 10^5\)
典中典外面套个二分,然后点分治维护 \(f_i\) 为长度为 \(i\) 的权值最大值,发现两个结构合并复杂度是 \(O(\max(len_x,len_y))\),所以按 \(len\) 排序,然后合并时单调队列做一下就行了。
时间复杂度 \(O(n\log n\log V)\),由于树形态不改变,为了卡常,可以提前记录点分治的形态,并将合并顺序记录下来。
P3292 [SCOI2016] 幸运数字
给出一棵 \(n\) 个点的树,有点权,然后有 \(m\) 次询问,每次询问给出 \(v,x,y\),询问 \(v\) 和 \(x\to y\) 上的点任意 xor 的最大值。
我们考虑点分治,发现只有 \(O(n\log n)\) 次线性基单点加入和 \(O(m)\) 次线性基合并。
然后我们直接做就好了,时间复杂度 \(O(n\log n\log V+m\log^2 V)\),空间复杂度 \(O(n\log V)\)。
有一个好写的前缀线性基做法。
LOJ6145
给出一棵 \(n\) 个点的树,\(m\) 次询问一个点 \(x\) 到编号在 \([l,r]\) 中的点的距离的最小值。
\(1\le n,m\le 2\times 10^5\)
我们考虑点分治,每次用 \(x\) 到分治中心的权值 + \([l,r]\) 到分治中心的最小值更新答案,这个做法支持可持久化后做强制在线。
还有一个做法是将 \(x\) 扔到 \(O(\log n)\) 个区间上,建虚树跑最短路。
可是虚树是十级内容,超纲了怎么办。
什么,这不是虚树,这是树上离散化,使用这个,复杂度变小变低。
所以我们只使用了离散化的知识
KDT
KDT 是最优正交范围搜索树,只有每次交换维度划分复杂度才是对的,且如果有插入,需要二进制分组,替罪羊式重构是错的。
二维范围修改查询问题:
1.对矩形中的元素进行一次修改
2.对矩形中的元素进行一次查询
修改对修改有结合律,修改对范围信息有分配律和结合律,范围信息对范围信息有交换律和结合律
有一个定理证明了在上面这个问题中,KDT是理论最优的范围修改查询数据结构,并且这个下界对离线情况依然成立
而二进制分组的查询复杂度是 \(O(\sqrt{n}+\sqrt{\frac n 2}+\sqrt{\frac n 4}+\ldots)=O(\sqrt n)\),而插入总复杂度是 \(O(\sum_{x=2^i}\frac n x x\log x)=O(n\log^2 n)\),十分的正确。
而且因为二进制分组带有时间顺序的信息,所以功能很完整。
注意:KDT 在查询信息不能剪枝时很慢,可以剪枝时更快。
KDT 在查询半平面,圆信息的时候全错,可以卡到飞起的。
Luogu3710 方方方的数据结构
第一行两个数 \(n,m\),表示数列的长度和操作个数。
接下来 \(m\) 行每行 \(2\) 或 \(4\) 个数。
- 如果第一个数为 \(1\),接下来跟三个数 \(l,r,d\),表示把区间 \([l,r]\) 中的数加上 \(d\)。
- 如果第一个数为 \(2\),接下来跟三个数 \(l,r,d\),表示把区间 \([l,r]\) 中的数乘上 \(d\)。
- 如果第一个数为 \(3\),接下来跟一个数 \(p\),表示询问 \(p\) 位置的数 \(\bmod\ 998244353\)。
- 如果第一个数为 \(4\),接下来跟一个数 \(p\),表示将第 \(p\) 行输入的操作撤销(保证为加或者乘操作,一个操作不会被撤销两次)。
我们离线可以得到每个操作的时间范围,那么加上上序列维度,我们就是矩阵加和乘,然后我们发现矩阵有 \(O(n^2)\) 个元素,单次操作 \(O(\sqrt {n^2})=O(n)\),鉴定为废了。
但是我们发现,我们只有 \(O(m)\) 个单点操作,那么我们的点数降到了 \(O(m)\),总时间复杂度 \(O(m\sqrt m)\)。
网上的神秘题
给一个长为 \(n\) 的序列 \(a\) 一个长为 \(n\) 的序列 \(b\)
\(b\) 是 \(0,1\) 序列
有 \(m\)次操作,每次操作给出一个 \(x\),将所有 \(a_i=x\) 的 \(b_i\) 异或 \(1\),求有多少连续的 \(1\) 段
我们发现 \(1\) 连续段个数等价于有多少点两边各为 \(01\)。
我们把每个点表示成平面上的 \((a_i,a_{i+1})\),那么每次就是行,列做取反操作,然后求 \(1\) 的个数。
好像是 SOJ 上的题

我们把每个点看作二位平面上的 \((i,rev_i)\),然后操作由于有均摊性,KDT 直接维护即可。
支配点对
- 初始有 n 个对象,两两对象之间可以产生一个贡献
- 每次给一个范围,求范围内的所有对象之间两两产生的贡献的信息合并
- 如果需要对两两对象计算贡献,并且每次询问需要求出范围内的两两对象的贡献,这样需要维护 \(O(n^2)\) 对贡献,即 \(O(n^2)\) 对二元组,复杂度过高
- 但是问题经常有性质,导致可以找出 \(O(n\log n)\) 对两两对象之间的贡献,只需要查询范围内这 \(O(n\log n)\) 对的贡献,即可覆盖 \(O(n^2)\) 对的贡献
- 这个 \(O(n\log n)\) 对二元组就叫做原问题的 \(O(n^2)\) 对二元组的一个支配集
- 找出支配集后一般问题会变成几种平凡形式
第一类支配对
- 两两对象产生一个贡献,总共产生 \(O(n^2)\) 对贡献,但是这些贡献中本质不同的只有 \(O(n)\) 种
- 常见的题型:树保留区间点
- 一般这种问题在树上是常见的,有的树上问题要范围内的点两两点求出 LCA,这样是求范围内两两对象的贡献,但是 LCA 只有本质不同的 \(n\) 个点
- 这种题的常见思路是做树上启发式合并,然后启发式合并时,加入点 \(x\) 时,只考虑编号为 \(x\) 的前驱后继的点和 \(x\) 产生二元组,这样会找出 \(O(n\log n)\) 个二元组,这些二元组可以支配原本\(O(n^2)\) 对二元组的贡献
P7880 [Ynoi2006] rldcot
给定一棵 \(n\) 个节点的树,树根为 \(1\),每个点有一个编号,每条边有一个边权。
定义 \(dep(x)\) 表示一个点到根简单路径上边权的和,\(lca(x,y)\) 表示 \(x,y\) 节点在树上的最近公共祖先。
共 \(m\) 组询问,每次询问给出 \(l,r\),求对于所有点编号的二元组 \((i,j)\) 满足 \(l \le i,j \le r\) ,有多少种不同的 \(dep( lca(i,j))\)。
\(1\le n\le 10^5,1\le m \le 5\times 10^5\)
支配点对板子,我们考虑一个点 \(x\) 在哪些情况下会成为 lca
-
\(l\le x\le r\)
-
在 \(x\) 两颗不同子树中,存在两个点 \(l\le a,b\le r\)
然后我们就有一个做法,我们对每个点 \(x\) 找到存在于两颗不同子树的所有点对 \((a,b)\) ,然后对询问扫描线,同时用树状数组维护每种深度在哪个位置产生贡献,这样的点对有 \(O(n^2)\) 个,此时复杂度为 \(O(n^2\log n+m\log n)\)。
然后我们发现,这 \(O(n^2)\) 个点对有很多是完全不必要的,比如设 \(x\) 子树中分别有来自三颗不同子树的点 \(a<b<c\),那么有可能产生贡献的点对只有 \((a,b)\) 和 \((b,c)\) ,而 \((a,c)\) 就不必要。
我们想想,发现只需要维护每个点在 \(x\) 其他子树的前驱后继,这一部分可以用树上启发式合并 + set 维护,此时点对数量也降到和树上启发式合并次数相同,为 \(O(n\log n)\),总时间复杂度为 \(O(n\log^2 n+m\log n)\)
P8528 [Ynoi2003] 铃原露露
给定一棵有根树,顶点编号为 \(1,2,\dots,n\),对 \(2\le i\le n\) 有 \(f_{i}\) 为 \(i\) 的父亲。\(a_1,\dots,a_n\) 是 \(1,\dots,n\) 的排列。
共 \(m\) 次询问,每次询问给出 \(l,r\),询问有多少个二元组 \(L,R\),满足 \(l\le L\le R\le r\),且对任意 \(L\le a_x\le a_y\le R\),有 \(x,y\) 在树上的最近公共祖先 \(z\) 满足 \(L\le a_z\le R\)。
\(1\le n,m\le 2\times 10^5\)
对于所有不好做,我们考虑哪些区间是不满足条件的。
- \(a_x<a_y<a_{lca(x,y)}\to l\in[1,a_x],r\in[a_y,a_z)\)
- \(a_{lca(x,y)}<a_x<a_y\to l\in(a_z,a_x],r\in[a_y,n]\)
所以我们 dsu on tree 一下,寻找前驱后继,得到 \(O(n\log n)\) 个矩形,然后我们转化成矩形加,矩形 \(0\) 个数,扫描线后转化成历史 \(0\) 个数,直接做就好了。
时间复杂度 \(O(n\log^2 n+m\log n)\)
第二类支配对
- 两两对象产生一个贡献,总共产生 \(O(n^2)\) 对贡献,但是这些贡献中本质不同的有 \(O(n^2)\) 种
常见的题型:区间内两两算个东西然后求 \(\min\) 或 \(\max\)- 有两种常见的支配对形式:
- 第一种是对每个 i,可以用数据结构高效找出 \(O(\log n)\) 个 \(j\),这些 \((i,j)\) 的贡献支配了所有 \((i,1),(i,2),...(i,n)\) 的贡献
- 第二种是对一维分治时,假设对大小 \(n\) 的问题进行分治,会产生 \(O(n)\) 对跨过分治中线的贡献,这些贡献支配了本来两边产生的 \(O(n^2)\) 对贡献,一般这种问题会对信息那一维分治,不对序列分治
- 第一种情况
- \((x_1,y_1)\) 的贡献可以支配 \((x_2,y_2)\) 的贡献,则 \((x_1,y_1)\) 构成的区间被 \((x_2,y_2)\) 构成的区间包含,并且统计 \((x_1,y_1)\) 的贡献后,统计 \((x_2,y_2)\) 的贡献一定不改变询问答案
- 首先 \((i,i+1)\) 的贡献一定是必要的,因为没有比 \((i,i+1)\) 小的区间
对每个 \(i\),我们先令 \(j=i+1\),计算 \((i,j)\) 的贡献
然后找到一个 \(k\),满足 \(j<k\),\((i,k)\) 有贡献,当且仅当 \((i,j),(j,k)\) 的贡献无法支配 \((i,k)\) 的贡献- 利用这一条性质对 \(a_k\) 的可行范围做限制,如果可以保证每次 \(a_k\) 可行范围减半,则对每个 \(i\) 最多有 \(O(\log n)\) 个 \(j\) 有贡献
CF765F Souvenirs
我们发现,对于 \(i<j<k,a_i<a_k<a_j\),如果 \(a_k-a_i\ge a_j-a_k\) 时,\((i,k)\) 这个点对就是没有贡献的,然后我们发现 \(a_k<\frac 1 2 (a_i+a_j)\),有效的 \((i,k)\) 点对只有 \(O(n\log V)\) 个,然后找出来扫描线即可。
NC262593 The Closest Pair
给定一个长度为 \(n\) 的序列 \(a\),保证 互不相同
\(q\) 次询问一个区间 \([l,r]\) 中选出两个数 \((a_i,a_j)\),\(\max(a_i,a_j)\bmod \min(a_i,a_j)\) 的最小值。
\(1\le n\le 10^5,1\le q\le 3\times 10^5,1\le V\le 10^6,6s\)
我们将 \(a\bmod b\) 转化成 \(a-\lfloor\frac a b \rfloor b\),然后我们不妨令贡献来自于 \(a_i>a_j,i<j\),对于另一种情况可以翻转再做一遍。
我们扫描时枚举 $\lfloor \frac a b\rfloor $,然后由于所有数互不相同,我们枚举的 \((\lfloor \frac a b\rfloor,b)\) 只有 \(O(V\log V)\) 对,且此时我们的 \(a\) 只能在一个值域区间内,然后我们先找出一个下标最大的 \(j\),满足 \(a_j\in [L,R]\),然后再找出一个下标最大的 \(k\in [L,a_j]\),然后我们发现,如果 \((k,i)\) 是一对有用的点对,那么就有 \(a_k\bmod a_i<a_j\bmod a_k\),即 \(a_k-L<a_j-a_k\),所以 \(a_k<\frac 1 2 (a_j+L)\),有用的 \(k\) 只有 \(O(\log V)\) 个,准确来说,所有有用点对数量最多为 \(O(\sum_{i=1}^n \frac V i\log i)=O(V\log V\log n)\) 对,我们对这些点对扫描线即可,时间复杂度 \(O(V\log V\log^2 n)\)。
P9058 [Ynoi2004] rpmtdq
给定一棵有边权的无根树,需要回答一些询问。
定义 \(\text{dist}(i,j)\) 代表树上点 \(i\) 和点 \(j\) 之间的距离。
对于每一组询问,会给出 \(l,r\),你需要输出 \(\min(\text{dist}(i,j))\) 其中 \(l\leq i < j \leq r\)。
\(1\le n\le 2\times 10^5,1\le q\le 10^6\)。
我们考虑对树分治,然后我们发现,合并两个结构时,设一个结构中有 \(x\),一个结构中有 \(i,j\),到分治中心的距离分别为 \(d_x,d_i,d_j\),且 \(\max(d_i,d_j)\le d_x\),那么如果 \(x<\min(i,j)\) 或者 \(\max(i,j)<x\),那么 \((x,i),(x,j)\) 就是无意义的(因为被 \((i,j)\) 支配),或者说,只有距离不大于 \(d_x\) 的 \(x\) 的前驱后继会出现贡献,这样我们在树上会找到 \(O(n\log n)\) 个点对,然后扫描线即可。
时间复杂度 \(O(n\log^2 n+m\log n)\)。

浙公网安备 33010602011771号