你对我来说还仅仅是『梦』|

Nityacke

园龄:2年11个月粉丝:20关注:24

lxl 讲课记录

线段树和平衡树

P7706 文文的摄影布置

给定一个有 n 个二元组的数组 (ai,bi),有三种操作,共 m 次。

  • 1 x vaxv

  • 2 x vbxv

  • 3 l r,询问 maxli<i+1<jrAi+Ajmink=i+1j1Bk

简单题,我们用线段树维护,发现答案合并时就来自两个子树的答案和 A,BAAB,A 共四种情况,我们维护一下 A,B,AB,BA 最大值即可。

我感觉可以修改可以推到区间上,因为内部相对大小不改变。

给定 n,m,w,有两种操作:

  • 单点修改
  • 询问区间是否能找出两个数使得其和为 w

1n,m,w5×105,4s

一个很弱智的想法是对于每个数找到其对应的前一个点,然后判断是否区间全部点的对应点下标都小于 L,但是这样复杂度是错的,这个题的好处是它有一个支配性质:

(i,j),(l,r) 满足条件且 li,jr,则 (l,r) 相比 (i,j) 一定不优。

所以我们每个点只在 O(1) 个点对里,此时复杂度正确。

而我们一个点点权的改变也只会造成 O(1) 个点的对应点变化,然后就可以直接维护。

均摊复杂度

序列染色段数均摊

  • 特点:修改有区间染色操作

  • 用平衡树维护区间的颜色连续段

  • 区间染色每次最多只会增加 O(1) 个连续颜色段,用平衡树维护所有连续段即可

  • 均摊的颜色段插入删除次数 O(n+m)

image

  • 应用:
  • 区间染色,维护区间的复杂信息
  • 区间排序
  • “ODT”类问题
  • 注意这里这个颜色段数均摊是有 23 的常数,常数很大

还有对于颜色段的区间修改:区间颜色段打标记一起动

  • 区间染色类问题的复杂形式

  • 一般会要求支持区间中每个颜色段进行移动,每个颜色段的移动是只需要考虑局部性质就可以确定的

  • 设计一个可以合并的,移动颜色段的标记即可

  • 需要注意颜色段长度变成 0 之后消失这种情况,处理时需要维护最短的连续段,当长度变成 0 时递归到叶子上找到这个颜色段并删除,同时修改局部的其他颜色段的局部相关信息

CF453E Little Pony and Lord Tirek

给一个序列,每个位置有初值 ai,最大值 mi,这个值每秒会增大 ri,直到 mi

m 个发生时间依此增大的询问,每次询问区间和并且将区间的所有数字变成 0

0n,m,ai,mi,ri105,0ti108

对于每一个数第一次被询问,我们显然可以暴力做。

我们对时间使用颜色段均摊,然后就相当于询问经过时间 T 后,每个位置会从 0 变成多少。

我们发现,对于 Tmiri 的元素,其贡献为 mi,否则为 Tri

我们对 miri 建立主席树,维护 miri,即可解决这个问题。

P5066 [Ynoi2014] 人人本着正义之名

你需要帮珂朵莉维护一个长为 n01 序列 a,有 m 个操作:

  • 1 l r:把区间 [l,r] 的数变成 0
  • 2 l r:把区间 [l,r] 的数变成 1
  • 3 l r[l,r1] 内所有数 ai,变为 aiai+1 按位或的值,这些数同时进行这个操作。
  • 4 l r[l+1,r] 内所有数 ai,变为 aiai1 按位或的值,这些数同时进行这个操作。
  • 5 l r[l,r1] 内所有数 ai,变为 aiai+1 按位与的值,这些数同时进行这个操作。
  • 6 l r[l+1,r] 内所有数 ai,变为 aiai1 按位与的值,这些数同时进行这个操作。
  • 7 l r:查询区间 [l,r] 的和。

本题强制在线,每次的 l,r 需要与上次答案做 xor 运算,如果之前没有询问,则上次答案为 0

我们用平衡树维护若干个 01 的连续段,那么每个操作相当于让每个段的端点根据其颜色向左右移动 ±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 逛街

image

lxl 锐评:所以这些比赛的出题人也没什么水平,出缝合题

我们同样考虑颜色段均摊,那么对于两端都大于两边的段,其每次操作长度增加 1

对于两端都小于的段,每次操作长度减小 1

其他情况长度不变。

然后同样维护这三种情况的数量,区间长度和,最小长度,如果出现长度为 0 就暴力回收,然后询问用类似于楼房重建的单侧递归方式即可。

P9061 Optimal Ordered Problem Solver

给定 n 个点 (xi,yi)i=1n,你需要按顺序处理 m 次操作。每次操作给出 o,x,y,X,Y

  • 首先进行修改:
    • o=1 则将满足 xix,yiy 的点的 yi 修改为 y
    • o=2 则将满足 xix,yiy 的点的 xi 修改为 x
  • 然后进行查询,询问满足 xiX,yiY 的点数。

1n,m106,1xi,yin

我们发现,被操作过的点构成了一条阶梯般的轮廓线,然后我们用平衡树维护这个轮廓线,由于这个轮廓线具有一个单调性,所以一次修改也是对这个平衡树的一段区间进行了 x 赋值或者 y 赋值,这个可以在平衡树上打标记维护。

修改时还需要我们找出新出现在轮廓线上的点,我们可以做一个扫描线,找到每个点第一次被覆盖的时间。

对于查询,我们做一个简单容斥,将询问分成三个部分减去全局总点数,需要在轮廓线上找出有贡献的点,这个可以在平衡树上二分。

对于还未被操作的点,是两个带修的 1−side 询问和一个未修改的 2−side 询问,二维数点即可。

这样我们就在 O((n+m)logn) 的时间复杂度内解决了问题。

被卡常了,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 的序列 ai,保证初始时所有数都是好的。

q 次操作,每次操作有三种可能:

  • 1 i 查询 ai
  • 2 l r xalr 赋值为一个好的数 x
  • 3 l r xalr 都加上 x,重复这一过程直到所有数都变好。

n,q105ai,x109

我们考虑在可能的值域 O(qx) 内处理出所有 42 的次幂,这个数量很少。

先考虑操作三,我们维护每个点还差多少到达下一个 42 的次幂,等价于区间减,然后如果这个差的最小值小于 0,就暴力找到该点,更新成下一个还差的权值,然后最小值为 0 说明还需要操作,由于势能分析,这样的复杂度为 O(nlognlog42V)

然后考虑加入二操作的情况,我们可以将每个颜色段同时维护,这样复杂度就正确了,每次操作只会带来 O(lognlog42V) 的势能。

这个可以平衡树无脑实现,也可以线段树在区间值全部相同的时候打标记实现。

CF702F T-Shirts

n 种 T 恤,每种有价格 ci 和品质 qi

m 个人要买 T 恤,第 i 个人有 vi 元,每人每次都会买一件能买得起的 qi 最大的 T 恤。一个人只能买一种 T 恤一件,所有人之间都是独立的。

问最后每个人买了多少件 T 恤?如果有多个 qi 最大的 T 恤,会从价格低的开始买。

先考虑暴力,我们由于买 T 恤的顺序的固定的,所以我们可以将 T 恤排序,然后一个一个判断。

然后我们考虑用平衡树维护这个过程,用平衡树维护每个人,枚举 T 恤,然后我们发现大于 c 的数会集体减去 c,然后我们发现对于一个有 v 元钱的人,有三种情况。

  • v<c,不用管。
  • cv<2c,此时减去 c 后,与 v<c 的部分相对顺序会改变,我们暴力提取出来修改,然后扔回平衡树里即可。
  • v2c,我们对 v 打上区间减标记,然后对 ans 打上加 1 标记即可。

然后对于暴力的第二部分,每一次修改后 v 至少减少一半,每个点最多被暴力修改 O(logv) 次,所以复杂度正确。

总时间复杂度 O(nlognlogv+mlogn)

扫描线

基础问题

CF1000F One Occurrence

给定长为 n 的序列,m 次查询区间中有多少数只出现一次。

1n,m5×105

我们考虑每种数在什么情况下有贡献,我们发现,对于一种出现在 a1,a2,ak 的数,这种数会对 l[ai1+1,ai],r[ai,ai+11] 的若干个矩形带来贡献,然后我们就将问题转化成了 O(n) 次矩形加,O(m) 次单点查询,扫描线解决。

#637. A. 数据结构

给一个长为 n 的序列,m 次查询:

如果将区间 [l,r] 中所有数都 +1,那么整个序列有多少个不同的数?

询问间独立,也就是说每次查询后这个修改都会被撤销

1n,m106

出现不太好考虑,我们考虑一种颜色 x 在什么情况下没有贡献。

包含全部 x,且不包含任何一个 x1,也就是对一些矩形的交有贡献,矩形的交还是矩形,还是可以扫描线做,时间复杂度 O((n+m)logn)

CF526F Pudding Monsters

给定一个 n×n 的棋盘,其中有 n 个棋子,每行每列恰好有一个棋子。

对于所有的 1kn,求有多少个 k×k 的子棋盘中恰好有 k 个棋子。

n3×105

我们将根据棋子每个列对应一个行,那么我们就是要统计 maxminlen=1 的区间数量,对 r 扫描线,然后由于 maxminlen1,所以我们统计最小值数量和最小值个数即可。

时间复杂度 O(nlogn)

区间子区间类模型

CF997E Good Subsegments

有一个1n的排列 P

如果区间[l,r]中的数是连续的,那么我们称它为好区间。

q次询问,每次问[l,r]内,有多少子区间是好的?

1n,q1.2×105

我们发现,这个题是上个题的加强版,我们考虑在上个问题的最小值个数上做历史版本和,具体来说,多两个数组,一个记录答案,一个标记记录要更新的次数,然后就可以了。

时间复杂度 O((n+q)logn)

CF103069G

给定一个序列,求区间有多少子区间,其内部出现过的颜色数为奇数

我们对 r 扫描线,发现操作等价于 01 序列区间异或,区间历史版本和,我们记录两个标记代表对 10 的答案贡献次数就可以了。

时间复杂度 O((n+m)logn)

换维扫描线

P3863序列

给定一个长度为 n 的序列,给出 q 个操作,形如:

1 l r x 表示将序列下标介于 [l,r] 的元素加上 x (请注意,x 可能为负)

2 p y 表示查询 ap 在过去的多少秒时间内不小于 y (不包括这一秒,细节请参照样例)

开始时为第 0 秒,第 i 个操作发生在第 i 秒。

我们发现,如果将序列维和时间维看成二维平面,那么我们的修改就是矩形加,然后查询是单列查询,我们对下标扫描线,维护时间维,那么我们的操作等价于后缀加,并查询区间不小于 y 的数的个数,分块维护。

复杂度 O(nnlogn)

P7560 [JOISC 2021 Day1] 饮食区

有一个长为 n 的序列,序列每个位置有个队列

m 个操作

  • 每个操作形如 [l,r] 的每个队列中进来了 ktype=c 的人

或者 [l,r] 的每个队列中出去了 k 个人(不足 k 个则全部出去)

还有查询某个队列中第 k 个人的 type(不足k个输出 0

1n.,m2.5×105

我们对下标扫描线,然后发现如果队列未清空过,那么我们查询第 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:对于 lirai:=fa(ai)

2 l r :查询对于 lir,最小的 dep(ai)

1n,m2×105,3s,64MB

我们考虑分块,然后最小值具有支配性质,具体的,如果 x 这个树上节点之前到达过,那么这个往上跳的节点都没用了,然后我们均摊一下每个块只会访问树上每个节点一次,然后对于散快操作,暴力重剖往上跳 K 级祖先即可,由于每个点只会被跳 O(logn) 次,不影响复杂度,逐块处理可以做到线性空间。

P5063 [Ynoi2014] 置身天上之森

线段树是一种特殊的二叉树,满足以下性质:
每个点和一个区间对应,且有一个整数权值;
根节点对应的区间是 [1,n]
如果一个点对应的区间是 [l,r],且 l<r,那么它的左孩子和右孩子分别对应区间 [l,m][m+1,r],其中 m=l+r2
如果一个点对应的区间是 [l,r],且 l=r,那么这个点是叶子;
如果一个点不是叶子,那么它的权值等于左孩子和右孩子的权值之和。
珂朵莉需要维护一棵线段树,叶子的权值初始为 0,接下来会进行 m 次操作:
操作 1:给出 l,r,a,对每个 xlxr),将 [x,x] 对应的叶子的权值加上 a,非叶节点的权值相应变化;
操作 2:给出 l,r,a,询问有多少个线段树上的点,满足这个点对应的区间被 [l,r] 包含,且权值小于等于 a

1n,m105

首先,我们发现线段树上只有 O(logn) 种节点大小,我们可以把节点大小相同的节点拉出来当一个序列,然后一次修改对每个序列的影响是区间加,然后边上的两块要特判一下,那么问题转化成了区间加区间小于等于某个数的个数,这个可以用分块轻松做到 O((n+m)nlogn) 的复杂度,然后有 O(logn) 个序列,看起来复杂度是 O((n+m)nlognlogn) 的,但仔细分析可以发现复杂度还是 O((n+m)(nlogn+n2logn2+n4logn4+))=O((n+m)nlogn),并且空间复杂度是 O(n+m),如果会分散层叠可以做到 O((n+m)n)

树套树和 CDQ 分治

[CQOI2011] 动态逆序对

典中典。

P3332 [ZJOI2013] K大数查询

你需要维护 n 个可重整数集,集合的编号从 1n
这些集合初始都是空集,有 m 个操作:

  • 1 l r c:表示将 c 加入到编号在 [l,r] 内的集合中
  • 2 l r c:表示查询编号在 [l,r] 内的集合的并集中,第 c 大的数是多少。

注意可重集的并是不去除重复元素的,如 {1,1,4}{5,1,4}={1,1,4,5,1,4}

我们把权值线段树套外面,然后对于内层维护的线段树,我们转化成区间加问题,然后对于询问,我们在外层权值线段树上二分,然后再内层线段树上查询即可。

时间复杂度 O(mlog2n)

P9068 [Ynoi Easy Round 2022] 超人机械 TEST_95

我们发现本质不同很困难,有一个经典升维然后带个 prei 计算,但是这里还有个利用了支配性质的做法。

我们对每种权值维护第一次出现和最后一次出现位置,比如记 w(x,y,0/1,t)xt 时刻变成了 y 第一次/最后一次出现的位置的贡献,那么贡献是:

w(x,y,0,t)=w(x,y,1,t)[x<x][y>y][t<t]w(x,y,1,t)=w(x,y,0,t)[x>x][y<y][t<t]

至于删除贡献加一维就可以了,时间复杂度 O(nlog2n),空间 O(n)

P4690 [Ynoi2016] 镜中的昆虫

区间染色,区间数颜色。

颜色段均摊简单题,修改直接把所有可能改变的点找出来暴力修改。

P3242 [HNOI2015] 接水果

给定树上 p 条带权路径,然后 m 次询问,每次给定一条路径 S 和一个数 k,询问被 S 包含的路径中,大小第 k 大的权值。

我们发现,那 p 条路径在二维平面上的的影响是 O(p) 个矩形,那么问题转化成把一个点包括的矩形的第 K 大权值,扫描线一下发现是区间每个集合加入一个元素,询问某个集合第 k 大,树套树即可。

可持久化

P5795 [THUSC2015] 异或运算

给定长度为 n 的数列 X=x1,x2,...,xn 和长度为 m 的数列 Y=y1,y2,...,ym,令矩阵 A 中第 i 行第 j 列的值 Ai,j=xi xor yj,每次询问给定矩形区域 i[u,d],j[l,r],找出第 k 大的 Ai,j

1n1000,1m300000,1p500

我们发现 n,p 很小,我们可以设计一个 O(nppolylog(m)) 的做法。

Y 中每个元素建出可持久化 Trie,然后每次询问拿 O(n) 个数在可持久化 Trie 上二分即可。

CF464E The Classic Problem

给定一张 n 个点,m 条边的无向图,每条边的边权为 2xi,求 st 的最短路,结果对 109+7 取模。

n,m,x105

典题,我们用线段树维护每一个二进制位为多少,然后一次修改就是去变成 0,单点修改,主席树维护最短路即可。

P7561 [JOISC 2021 Day2] 道路の建設案 (Road Construction)

给出平面上 n 个点和一个数 k,请输出所有点对两两曼哈顿距离的前 k 小值。

1n,k2.5×105

我们考虑类似于超级钢琴的套路,那么问题转化成:每个点有一个平面,初始时此平面的点集就是所有点的集合,然后不断在此平面里查询距离这个点最近的点和删点。

对于询问距离,我们把贡献拆成 4 份,分开维护,对每个点维护左上,左下,右上,右下的最小贡献,这一部分用扫描线+主席树维护,然后删点新建一个版本即可。

时间复杂度 O((n+k)logn),不过看起来常数巨大,还巨难写,我写了 3KB 后扔了。

但是还有一些很好写的做法,转切比雪夫距离,二分答案,然后按 X 排序,满足条件的点对在一个区间中,可以双指针维护,然后用 set 按 Y 排序,维护这些点对,不难发现满足条件的点对也是一段区间,暴力统计点对,数量达到 k 就退出,这个做法的复杂度是 O((n+k)lognlogV) 的,而且相当好写。

还有一个做法,转切比雪夫距离,二分答案,平面分块,发现能出现的点对在其周围的 O(1) 个块中,具体的,加入每个点时,暴力统计周围块的点判断是否合法,点对数达到 k 就退出,然后发现这个查找的总不合法点对数量是 O(n+k) 的,时间复杂度 O((n+k)logV),常数在哈希表上。

树上问题

P1600 天天爱跑步

经典题,题意自己看吧。

我们发现,一条 uv 的路径会对 x 造成贡献的条件是:

  • usubtreeu[dxax=du]
  • vsubtreex[dxax=2dlcadu]

发现等价于子树中一个数出现次数,做法很多,可以线段树合并,转化成 dfs 子树前后桶大小拆分。

CF757G Can Bash Save the Day?

一棵 n 个点的树和一个排列 pi,边有边权,支持两种操作:

  • l r x,询问 i=lrdis(pi,x)
  • x,交换 px,px+1

n,q2×105,强制在线。

我们考虑对询问差分,然后这个 dis 我们可以用 P4211 LCA 的套路,转化成链加链求和,对于强制在线,主席树即可。

然后我们发现对于询问,我们只会改变 x 版本的主席树,直接修改即可,时空复杂度 O((n+m)log2n),比较卡常,需要在操作进行一半的时候全部重构不然空间会炸,正解是 O((n+m)logn),可持久化边分树。

你说得对,但是 lxl 说 lct 可以可持久化,不知道能不能做单 log

CF1017G The Tree

给定一棵树,维护以下3个操作:

  • 1 x表示如果节点 x 为白色,则将其染黑。否则对这个节点的所有儿子递归进行相同操作

  • 2 x表示将以节点 x 为根的子树染白。

  • 3 x表示查询节点 x 的颜色

我们发现,如果把一次 1 操作看成单点 +1,所有点初值为 1,一个节点被染黑的条件是 xrt 的后缀最大值 0,然后对于 2 操作,一个很 trival 的想法是直接子树赋值成 1,但我们发现 xrt 的后缀最大值此时不一定为 1,我们要在 x 上减去这个最大值,也就是除去 fxrt 的贡献,这样就是正确的。

LOJ6276

树,点有颜色,有多少链满足上面的颜色互不相同
每种颜色出现次数<=20,n1e5,4s

我们考虑找到哪些是不合法的,我们枚举同种颜色的两两点对,然后包含这个点对的路径在 dfn 序上是 O(1) 个矩形,转化成矩形加,全局 0 个数,扫描线即可。

时间复杂度 O(cnlogn)

树分治

  • 一般维护路径信息的话点分治和链分治比较好写

  • 维护子树信息的话链分治会很方便

  • 边分治用来分析一些问题会比较方便,因为进行了点度数的分治

  • 具体问题需要考虑具体使用哪种树分治会更简单,每种树分治有其优点和缺点

点分治

  • 序列上的分治是每次找一个中点,然后统计经过中点的区间,然后递归下去计算

  • 树的点分治每次找一个重心,然后统计经过重心的路径,然后把这个点删去,树变成了很多连通子图,递归下去计算

  • 我们每次递归下去的连通子图大小至少减半,所以递归树的深度 O(logn),所以点分治复杂度 O(nlogn)

点分治问题在于两两子树贡献的统计时,如果你直接做,可能会加上一个序列维的限制,就会很唐。

但是如果合并两个大小为 O(a),O(b) 的子树复杂度为 O(a+b) 的话,直接做复杂度又会假。

正确的方法是使用类似于哈夫曼树的方法,每次选出用堆最小的两个结构来合并,可以证明如果合并两个大小为 O(a),O(b) 的子树复杂度为 O(a+b) 的话,那么点分治的复杂度为 O(nlogn),且优势在于我们只需要维护两个结构相互的贡献,在很多情况下可以代替边分治。

边分治

  • 树的边分治每次找一条边,然后统计经过这条边的路径,然后把这条边删去,树变成了两个连通子图,递归下去计算
  • 分治一般是想让分治出的每个子问题大小接近,所以我们尽可能让两边大小接近
  • 实际上树的边分治更好地对应到了序列的分治

但是菊花图的时候复杂度会假,我们需要对树进行三度化,这样可以证明,每次子树大小最大只会是原来的 23,而且这个边分树会成为一颗二叉树,我们可以在上面做可持久化,边分树合并之类的神秘操作

链分治

  • 基于轻重链剖分的结构,可以看作是每次删去一条重链之后继续分治下去
  • 实际上和树上启发式合并是等价的:我们观察启发式合并的时候,我们每次是把size小的子树插入size最大的子树,这个可以看做是这个size大的子树所在的重链被删除了,然后依次合并重链上的轻儿子到这个重儿子上

例题

P5314 [Ynoi2011] ODT

给你一棵树,边权为 1,有点权。

需要支持两个操作:

  • 1 x y z:表示把树上 xy 这条简单路径的所有点点权都加上 z
  • 2 x y:表示查询与点 x 距离小于等于 1 的所有点里面的第 y 小点权。

首先我们知道考虑链分治,一个节点最多有 O(1) 个邻域的点不是重链头,而且我们知道一次链加只会访问 O(1) 个重链头,然后我们就可以对每个结点维护一颗平衡树,维护其轻儿子的权值,修改时在重链头的父亲的平衡树中修改,查询时暴力插入自己,父亲,重儿子三个节点的权值即可,修改复杂度 O(log2n),查询复杂度 O(logn),不平衡,我们可以对每个点维护其子树大小 O(a) 大的儿子,则一次修改复杂度为 O(loganlogn),查询复杂度 O(alogn),由于复杂度平衡,所以 O(logan)=O(a),a=O(lognloglogn),总时间复杂度 O(nlogn+mlog2nloglogn)

存在基于平衡树多树二分的 O((n+m)logn) 做法。

随便 YY 的题

给一棵树,点有点权,求所有路径中点权 xor 和最大的一条路径

Sol1:点分治

我们开一棵 Trie,维护插入的点到分治中心构成链的 xor 和,然后就是简单的查询和插入。

Sol2:边分治

需要三度化,优势是只需要合并两颗子树,虽然此题中无优越性。

Sol3:链分治

考虑启发式合并的过程,合并轻儿子前查询贡献,发现重儿子继承时会将 Trie 全局 xor 上一个值,我们可以懒惰处理,记录全局 xor 了 x,查询和插入时都 xor 上 x 即可。

CF150E Freezing with Style

给定一颗带边权的树,求一条边数在 [L,R] 之间的路径,并使得路径上边权的中位数最大。输出一条可行路径的两个端点。

1n105

典中典外面套个二分,然后点分治维护 fi 为长度为 i 的权值最大值,发现两个结构合并复杂度是 O(max(lenx,leny)),所以按 len 排序,然后合并时单调队列做一下就行了。

时间复杂度 O(nlognlogV),由于树形态不改变,为了卡常,可以提前记录点分治的形态,并将合并顺序记录下来。

P3292 [SCOI2016] 幸运数字

给出一棵 n 个点的树,有点权,然后有 m 次询问,每次询问给出 v,x,y,询问 vxy 上的点任意 xor 的最大值。

我们考虑点分治,发现只有 O(nlogn) 次线性基单点加入和 O(m) 次线性基合并。

然后我们直接做就好了,时间复杂度 O(nlognlogV+mlog2V),空间复杂度 O(nlogV)

有一个好写的前缀线性基做法。

LOJ6145

给出一棵 n 个点的树,m 次询问一个点 x 到编号在 [l,r] 中的点的距离的最小值。

1n,m2×105

我们考虑点分治,每次用 x 到分治中心的权值 + [l,r] 到分治中心的最小值更新答案,这个做法支持可持久化后做强制在线。

还有一个做法是将 x 扔到 O(logn) 个区间上,建虚树跑最短路。

可是虚树是十级内容,超纲了怎么办。

什么,这不是虚树,这是树上离散化,使用这个,复杂度变小变低。

所以我们只使用了离散化的知识

KDT

KDT 是最优正交范围搜索树,只有每次交换维度划分复杂度才是对的,且如果有插入,需要二进制分组,替罪羊式重构是错的。

二维范围修改查询问题:
1.对矩形中的元素进行一次修改
2.对矩形中的元素进行一次查询
修改对修改有结合律,修改对范围信息有分配律和结合律,范围信息对范围信息有交换律和结合律
有一个定理证明了在上面这个问题中,KDT是理论最优的范围修改查询数据结构,并且这个下界对离线情况依然成立

而二进制分组的查询复杂度是 O(n+n2+n4+)=O(n),而插入总复杂度是 O(x=2inxxlogx)=O(nlog2n),十分的正确。

而且因为二进制分组带有时间顺序的信息,所以功能很完整。

注意:KDT 在查询信息不能剪枝时很慢,可以剪枝时更快。

KDT 在查询半平面,圆信息的时候全错,可以卡到飞起的。

Luogu3710 方方方的数据结构

第一行两个数 n,m,表示数列的长度和操作个数。

接下来 m 行每行 24 个数。

  1. 如果第一个数为 1,接下来跟三个数 l,r,d,表示把区间 [l,r] 中的数加上 d
  2. 如果第一个数为 2,接下来跟三个数 l,r,d,表示把区间 [l,r] 中的数乘上 d
  3. 如果第一个数为 3,接下来跟一个数 p,表示询问 p 位置的数 mod 998244353
  4. 如果第一个数为 4,接下来跟一个数 p,表示将第 p 行输入的操作撤销(保证为加或者乘操作,一个操作不会被撤销两次)。

我们离线可以得到每个操作的时间范围,那么加上上序列维度,我们就是矩阵加和乘,然后我们发现矩阵有 O(n2) 个元素,单次操作 O(n2)=O(n),鉴定为废了。

但是我们发现,我们只有 O(m) 个单点操作,那么我们的点数降到了 O(m),总时间复杂度 O(mm)

网上的神秘题

给一个长为 n 的序列 a 一个长为 n 的序列 b
b0,1 序列
m次操作,每次操作给出一个 x,将所有 ai=xbi 异或 1,求有多少连续的 1

我们发现 1 连续段个数等价于有多少点两边各为 01

我们把每个点表示成平面上的 (ai,ai+1),那么每次就是行,列做取反操作,然后求 1 的个数。

好像是 SOJ 上的题

image

我们把每个点看作二位平面上的 (i,revi),然后操作由于有均摊性,KDT 直接维护即可。

支配点对

  • 初始有 n 个对象,两两对象之间可以产生一个贡献
  • 每次给一个范围,求范围内的所有对象之间两两产生的贡献的信息合并
  • 如果需要对两两对象计算贡献,并且每次询问需要求出范围内的两两对象的贡献,这样需要维护 O(n2) 对贡献,即 O(n2) 对二元组,复杂度过高
  • 但是问题经常有性质,导致可以找出 O(nlogn) 对两两对象之间的贡献,只需要查询范围内这 O(nlogn) 对的贡献,即可覆盖 O(n2) 对的贡献
  • 这个 O(nlogn) 对二元组就叫做原问题的 O(n2) 对二元组的一个支配集
  • 找出支配集后一般问题会变成几种平凡形式

第一类支配对

  • 两两对象产生一个贡献,总共产生 O(n2) 对贡献,但是这些贡献中本质不同的只有 O(n)
  • 常见的题型:树保留区间点
  • 一般这种问题在树上是常见的,有的树上问题要范围内的点两两点求出 LCA,这样是求范围内两两对象的贡献,但是 LCA 只有本质不同的 n 个点
  • 这种题的常见思路是做树上启发式合并,然后启发式合并时,加入点 x 时,只考虑编号为 x 的前驱后继的点和 x 产生二元组,这样会找出 O(nlogn) 个二元组,这些二元组可以支配原本O(n2) 对二元组的贡献

P7880 [Ynoi2006] rldcot

给定一棵 n 个节点的树,树根为 1,每个点有一个编号,每条边有一个边权。

定义 dep(x) 表示一个点到根简单路径上边权的和,lca(x,y) 表示 x,y 节点在树上的最近公共祖先。

m 组询问,每次询问给出 l,r,求对于所有点编号的二元组 (i,j) 满足 li,jr ,有多少种不同的 dep(lca(i,j))

1n105,1m5×105

支配点对板子,我们考虑一个点 x 在哪些情况下会成为 lca

  • lxr

  • x 两颗不同子树中,存在两个点 la,br

然后我们就有一个做法,我们对每个点 x 找到存在于两颗不同子树的所有点对 (a,b) ,然后对询问扫描线,同时用树状数组维护每种深度在哪个位置产生贡献,这样的点对有 O(n2) 个,此时复杂度为 O(n2logn+mlogn)

然后我们发现,这 O(n2) 个点对有很多是完全不必要的,比如设 x 子树中分别有来自三颗不同子树的点 a<b<c,那么有可能产生贡献的点对只有 (a,b)(b,c) ,而 (a,c) 就不必要。

我们想想,发现只需要维护每个点在 x 其他子树的前驱后继,这一部分可以用树上启发式合并 + set 维护,此时点对数量也降到和树上启发式合并次数相同,为 O(nlogn),总时间复杂度为 O(nlog2n+mlogn)

P8528 [Ynoi2003] 铃原露露

给定一棵有根树,顶点编号为 1,2,,n,对 2infii 的父亲。a1,,an1,,n 的排列。

m 次询问,每次询问给出 l,r,询问有多少个二元组 L,R,满足 lLRr,且对任意 LaxayR,有 x,y 在树上的最近公共祖先 z 满足 LazR

1n,m2×105

对于所有不好做,我们考虑哪些区间是不满足条件的。

  • ax<ay<alca(x,y)l[1,ax],r[ay,az)
  • alca(x,y)<ax<ayl(az,ax],r[ay,n]

所以我们 dsu on tree 一下,寻找前驱后继,得到 O(nlogn) 个矩形,然后我们转化成矩形加,矩形 0 个数,扫描线后转化成历史 0 个数,直接做就好了。

时间复杂度 O(nlog2n+mlogn)

第二类支配对

  • 两两对象产生一个贡献,总共产生 O(n2) 对贡献,但是这些贡献中本质不同的有 O(n2)
    常见的题型:区间内两两算个东西然后求 minmax
  • 有两种常见的支配对形式:
  • 第一种是对每个 i,可以用数据结构高效找出 O(logn)j,这些 (i,j) 的贡献支配了所有 (i,1),(i,2),...(i,n) 的贡献
  • 第二种是对一维分治时,假设对大小 n 的问题进行分治,会产生 O(n) 对跨过分治中线的贡献,这些贡献支配了本来两边产生的 O(n2) 对贡献,一般这种问题会对信息那一维分治,不对序列分治
  • 第一种情况
  • (x1,y1) 的贡献可以支配 (x2,y2) 的贡献,则 (x1,y1) 构成的区间被 (x2,y2) 构成的区间包含,并且统计 (x1,y1) 的贡献后,统计 (x2,y2) 的贡献一定不改变询问答案
  • 首先 (i,i+1) 的贡献一定是必要的,因为没有比 (i,i+1) 小的区间
    对每个 i,我们先令 j=i+1,计算 (i,j) 的贡献
    然后找到一个 k,满足 j<k,(i,k) 有贡献,当且仅当 (i,j)(j,k) 的贡献无法支配 (i,k) 的贡献
  • 利用这一条性质对 ak 的可行范围做限制,如果可以保证每次 ak 可行范围减半,则对每个 i 最多有 O(logn)j 有贡献

CF765F Souvenirs

我们发现,对于 i<j<k,ai<ak<aj,如果 akaiajak 时,(i,k) 这个点对就是没有贡献的,然后我们发现 ak<12(ai+aj),有效的 (i,k) 点对只有 O(nlogV) 个,然后找出来扫描线即可。

NC262593 The Closest Pair

给定一个长度为 n 的序列 a,保证 互不相同

q 次询问一个区间 [l,r] 中选出两个数 (ai,aj)max(ai,aj)modmin(ai,aj) 的最小值。

1n105,1q3×105,1V106,6s

我们将 amodb 转化成 aabb,然后我们不妨令贡献来自于 ai>aj,i<j,对于另一种情况可以翻转再做一遍。

我们扫描时枚举 ab,然后由于所有数互不相同,我们枚举的 (ab,b) 只有 O(VlogV) 对,且此时我们的 a 只能在一个值域区间内,然后我们先找出一个下标最大的 j,满足 aj[L,R],然后再找出一个下标最大的 k[L,aj],然后我们发现,如果 (k,i) 是一对有用的点对,那么就有 akmodai<ajmodak,即 akL<ajak,所以 ak<12(aj+L),有用的 k 只有 O(logV) 个,准确来说,所有有用点对数量最多为 O(i=1nVilogi)=O(VlogVlogn) 对,我们对这些点对扫描线即可,时间复杂度 O(VlogVlog2n)

P9058 [Ynoi2004] rpmtdq

给定一棵有边权的无根树,需要回答一些询问。

定义 dist(i,j) 代表树上点 i 和点 j 之间的距离。

对于每一组询问,会给出 l,r,你需要输出 min(dist(i,j)) 其中 li<jr

1n2×105,1q106

我们考虑对树分治,然后我们发现,合并两个结构时,设一个结构中有 x,一个结构中有 i,j,到分治中心的距离分别为 dx,di,dj,且 max(di,dj)dx,那么如果 x<min(i,j) 或者 max(i,j)<x,那么 (x,i),(x,j) 就是无意义的(因为被 (i,j) 支配),或者说,只有距离不大于 dxx 的前驱后继会出现贡献,这样我们在树上会找到 O(nlogn) 个点对,然后扫描线即可。

时间复杂度 O(nlog2n+mlogn)

本文作者:Nityacke

本文链接:https://www.cnblogs.com/Nityacke/p/lxltxdy.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Nityacke  阅读(145)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起