复习笔记——数据结构

并查集

普通并查集

并查集又快又妙,维护连通性超级方便,千万别忘了它
有时删边操作逆过来可看成加边,并查集维护

路径压缩,有时不过脑子就写错了,注意注意!!

int fa[N];
int getfa(int x) { return x==fa[x]?x:fa[x]=getfa(fa[x]); }
void unit(int x,int y) { x=getfa(x); y=getfa(y); fa[x]=y; }

带权并查集

\(getfa\) 操作路径压缩过程中顺便更新权值
一般是维护边权、异或啥的,更新时注意细节


线段树

普通线段树

支持区间加(打tag标记)、覆盖、查询
一些模板

int cnt,root,ch[N*2][2];
ll sum[N*2],tag[N*2];

void update(int x) { sum[x]=sum[ch[x][0]]+sum[ch[x][1]]; }
void build(int x,int l,int r){
	if(l==r) { sum[x]=a[l]; return; }
	int mid=(l+r)>>1;
	build(ch[x][0]=++cnt,l,mid);
	build(ch[x][1]=++cnt,mid+1,r);
	update(x);
}
void push(int x,int l,int r,ll c){
	tag[x]+=c;
	sum[x]+=c*(r-l+1);
}
void pushdown(int x,int l,int r){
	if(!tag[x]) return;
	int mid=(l+r)>>1;
	push(ch[x][0],l,mid,tag[x]);
	push(ch[x][1],mid+1,r,tag[x]);
	tag[x]=0;
}
void modify(int x,int l,int r,int L,int R,ll c){
	if(L<=l && r<=R) { push(x,l,r,c); return; }
	pushdown(x,l,r);
	int mid=(l+r)>>1;
	if(L<=mid) modify(ch[x][0],l,mid,L,R,c);
	if(R>mid) modify(ch[x][1],mid+1,r,L,R,c);
	update(x);
}
ll Sum(int x,int l,int r,int L,int R){
	if(L<=l && r<=R) return sum[x];
	pushdown(x,l,r);
	int mid=(l+r)>>1;
	ll ret=0;
	if(L<=mid) ret+=Sum(ch[x][0],l,mid,L,R);
	if(R>mid) ret+=Sum(ch[x][1],mid+1,r,L,R);
	return ret;
}

支持单点修改、最大子段和(维护区间内最大子段和、包含左端点或右端点的最大子段)
支持区间加乘结合(打两个tag,表示先乘后加,push的时候记得都要更新)
。。。。。

李超线段树

动态维护一些直线(支持插入直线),支持查询 与 \(x=x_0\) 相交的直线中最上 \(or\) 最下的一条是谁

精髓:维护区间最佳直线
以询问交点纵坐标 \(min\) 为例
在某个区间加入一条直线:
1)当前区间无最佳直线:将该直线设为该区间最佳直线,返回
2)在当前区间完全>最佳直线:不可能更优,直接返回
3)在当前区间完全<最佳直线:更优,将最佳直线设为当前这条,返回
4)区间中有一部分最佳直线更优,一部分当前直线更优:两条中一定有一条更优的部分更小(小于等于区间长度的一半),把大的那条设为当前区间的最佳直线,小的那条往它更优的那半边子树下放,在新的区间中继续操作

db K[N],B[N];//y=Kx+B
int cnt,root,ch[M*2][2],mx[M*2];
void build(int x,int l,int r){
	mx[x]=0;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(ch[x][0]=++cnt,l,mid);
	build(ch[x][1]=++cnt,mid+1,r);
}
db cal(int a,int x){ return K[a]*x+B[a]; }
bool better(int a,int b,int x){
	if(a==0) return false;
	if(b==0) return true;
	db ya=cal(a,x),yb=cal(b,x);
	if(fabs(ya-yb)<eps) return a<b;
	return ya>yb;
}
void ins(int x,int l,int r,int L,int R,int c){
	if(L<=l && r<=R){
		int mid=(l+r)>>1;
		if(better(c,mx[x],mid)) swap(c,mx[x]);
		int tl=better(mx[x],c,l),tr=better(mx[x],c,r);
		if(l==r || (tl && tr)) return;
		if(tl) ins(ch[x][1],mid+1,r,L,R,c);
		else ins(ch[x][0],l,mid,L,R,c);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) ins(ch[x][0],l,mid,L,R,c);
	if(R>mid) ins(ch[x][1],mid+1,r,L,R,c);
}
int Max(int x,int l,int r,int c){
	if(l==r) return mx[x];
	int mid=(l+r)>>1,ret;
	if(c<=mid) ret=Max(ch[x][0],l,mid,c);
	else ret=Max(ch[x][1],mid+1,r,c);
	if(better(mx[x],ret,c)) ret=mx[x];
	return ret;
}

吉司机线段树

线段树合并

一般合并的都是权值线段树,大多与树结构相关
只有两颗树共有的节点才会往下访问,所以很快的。

int merge(int x,int y){
    if(!x || !y) return x+y;
    sum[x]+=y;
    ch[x][0]=merge(ch[x][0],ch[y][0]);
    ch[x][1]=merge(ch[x][1],ch[y][1]);
    return x;
}

树状数组

一维树状数组

主要用途有三种:

  1. 维护原序列,支持单点修改、区间求和( \(sum(x)\) 求得是 \(\sum\limits_{i=1}^x a_i\) )
int n;
ll c[N];
void add(int x,ll y) { while(x<=n) c[x]+=y,x+=x&(-x); }
ll sum(int x){
	ll ret=0;
	while(x) ret+=c[x],x-=x&(-x);
	return ret;
}
单点x+y:add(x,y)
查询[l,r]的和:sum(r)-sum(l-1)
  1. 维护差分序列,支持区间修改、单点求值 (区间修改的时候注意要差分!\(sum(x)\) 求的是 \(a_x\) )
函数同上
区间[l,r]+y:add(l,y),add(r+1,-y)
单点x求值:sum(x)
  1. 维护差分序列及差分序列 \(\times id\),支持区间修改、区间求值
    设差分序列为 \(d[i]=a[i]-a[i-1]\),则 \(a[i]=\sum\limits_{j=1}^i d[j]\)
    \(\sum\limits_{i=1}^na[i]=\sum\limits_{i=1}^n\sum\limits_{j=1}^i d[i]=\sum\limits_{i=1}^n d[i]\times(n-i+1)\)
    所以维护两个树状数组,分别表示 \(d[i]\)\(d[i]\times i\)
int n;
ll c[N],ci[N];
void add(int x,ll y) { 
	ll id=x;
	while(x<=n) c[x]+=y,ci[x]+=y*id,x+=x&(-x); 
}
ll sum(int x){
	ll ret=0,id=x;
	while(x) ret+=c[x]*(id+1)-ci[x],x-=x&(-x);
	return ret;
}
区间[l,r]+y:add(l,y),add(r+1,-y)
区间[l,r]求和:sum(r)-sum(l-1)

其他用途:作为树套树中的外围

二维树状数组

主要用途有三种:

  1. 单点修改,区间查询
    树状数组套树状数组哈哈~
int n,m;
ll c[N][N];
void add(int x,int y,int val){
	for(int i=x;i<=n;i+=i&(-i))
		for(int j=y;j<=m;j+=j&(-j))
			c[i][j]+=val;
}
ll sum(int x,int y){
	ll ret=0;
	for(int i=x;i>0;i-=i&(-i))
		for(int j=y;j>0;j-=j&(-j))
			ret+=c[i][j];
	return ret;
}
  1. 区间修改,单点查询
    以前的博客
  2. 区间修改,区间查询
    以前的博客
    贴个代码吧
int n,m;
ll c[N][N],ci[N][N],cj[N][N],cij[N][N];
void add(int x,int y,int val){
	for(int i=x;i<=n;i+=i&(-i))
		for(int j=y;j<=m;j+=j&(-j)){
			c[i][j]+=val;
			ci[i][j]+=1ll*x*val;
			cj[i][j]+=1ll*y*val;
			cij[i][j]+=1ll*x*y*val;
		}
}
ll sum(int x,int y){
	ll ret=0;
	for(int i=x;i>0;i-=i&(-i))
		for(int j=y;j>0;j-=j&(-j))
			ret+=c[i][j]*(x+1)*(y+1)-ci[i][j]*(y+1)-cj[i][j]*(x+1)+cij[i][j];
	return ret;
}

平衡树

Treap

推荐以前的博客
维护一个有序序列,支持插入、删除,询问前驱后继、排名等
就是在二叉搜索树基础上每个点加了随机值用来维护形态

删除的细节有点多,注意随时判断 \(root\) 的变化!

int cnt,root,rf,pa[N],ch[N][2],val[N],rnd[N],sz[N];
void update(int x){ sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]]; }
void rotate(int x,int ty){
	int fa=pa[x],son=ch[x][ty^1],gp=pa[fa];
	ch[fa][ty]=son; if(son) pa[son]=fa;
	ch[x][ty^1]=fa; pa[fa]=x;
	pa[x]=gp; ch[gp][fa==ch[gp][1]]=x;
	if(fa==root) root=x;
	update(fa); update(x);
}
int rr;
int getrnd() { return rr=1ll*rr*4987657%39916801; }
void ins(int x,int nd){
	int f=val[x]<val[nd];
	if(!ch[x][f]) ch[x][f]=nd,pa[nd]=x;
	else ins(ch[x][f],nd);
	update(x);
	if(rnd[ch[x][f]]>rnd[x]) rotate(ch[x][f],f);
}
int find(int x,int k){
	if(sz[ch[x][0]]>=k) return find(ch[x][0],k);
	if(sz[ch[x][0]]==k-1) return x;
	return find(ch[x][1],k-1-sz[ch[x][0]]);
}
int getrk(int x,int k){
	if(!x) return 0;
	if(val[x]<=k) return sz[ch[x][0]]+1+getrk(ch[x][1],k);
	return getrk(ch[x][0],k);
}
int pre(int x,int k){
	if(!x) return -INF;
	if(val[x]<k) return max(pre(ch[x][1],k),val[x]);
	return pre(ch[x][0],k); 
}
int sub(int x,int k){
	if(!x) return INF;
	if(val[x]>k) return min(sub(ch[x][0],k),val[x]);
	return sub(ch[x][1],k);
}
void del(int x,int k){
	if(val[x]==k){
		if(ch[x][0] && ch[x][1]) {
			int f=rnd[ch[x][0]]<rnd[ch[x][1]];
			rotate(ch[x][f],f);
			del(x,k);
		}
		else {
			int f=ch[x][0]?0:1;
			ch[pa[x]][x==ch[pa[x]][1]]=ch[x][f];
			if(ch[x][f]) pa[ch[x][f]]=pa[x];
			if(x==root) root=ch[x][f]; //don't forget!
			x=pa[x];
			while(x!=rf) update(x),x=pa[x];
		}
	}
	else del(ch[x][k>val[x]],k);
}

FHQ-Treap

非旋转 \(treap\) ,关键操作是 \(merge\) 将两棵树合并 ,\(split\) 将一棵树按权值或大小分为两棵树;\(find\) 查找排名为 \(k\) 的数也很重要,有时可以替掉按大小 \(split\)
维护一个有序序列,支持插入、删除,询问前驱后继、排名等;区间操作也是没问题的!(所以能取代 \(splay\) 的很多操作)
更重要的是支持可持久化

int cnt,root,ch[N*2][2],sz[N*2],val[N*2],rnd[N*2];
int rr;
int getrnd() { return rr=1ll*rr*44987657%39916801; }
void update(int x) { sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]]; }
int merge(int x,int y){
	if(!x || !y) return x+y;
	if(rnd[x]>rnd[y]){
		ch[x][1]=merge(ch[x][1],y);
		update(x); return x;
	}
	else{
		ch[y][0]=merge(x,ch[y][0]);
		update(y); return y;
	}
}
Pr split(int x,int k){ //by val
	if(!x) return Pr(0,0);
	Pr z;
	if(val[x]<=k){
		z=split(ch[x][1],k);
		ch[x][1]=z.fi; update(x);
		return Pr(x,z.se);
	}
	else{
		z=split(ch[x][0],k);
		ch[x][0]=z.se; update(x);
		return Pr(z.fi,x);
	}
}
int find(int x,int k){ //in tree x,rk k
	if(sz[ch[x][0]]>=k) return find(ch[x][0],k);
	if(sz[ch[x][0]]+1==k) return x;
	return find(ch[x][1],k-1-sz[ch[x][0]]);
}
//-------------------------------------
插入x:
tmp=++cnt;
ch[tmp][0]=ch[tmp][1]=0; sz[tmp]=1; val[tmp]=x; rnd[tmp]=getrnd();
if(!root) root=tmp;
else {
	Pr w=split(root,x);
	root=merge(merge(w.fi,tmp),w.se);
}
删除x:
Pr w=split(root,x),z=split(w.fi,x-1);
root=merge(z.fi,merge(merge(ch[z.se][0],ch[z.se][1]),w.se));
查排名:
Pr w=split(root,x-1); 
printf("%d\n",sz[w.fi]+1);
root=merge(w.fi,w.se);
前驱:
Pr w=split(root,x-1);
printf("%d\n",val[find(w.fi,sz[w.fi])]);
root=merge(w.fi,w.se);
后继:
Pr w=split(root,x);
printf("%d\n",val[find(w.se,1)]);
root=merge(w.fi,w.se);

Splay

\(treap\) 的基础上可以把一个点旋转到指定位置,可以更方便的进行区间操作。
(貌似大多数操作都能用 \(FHQ-Treap\) 代替……但还是很方便…)
对区间 \([l,r]\) 操作时,都是预先在序列前后各加一个数,然后把 第 \(l-1\) 个数转到 \(rf\) 之下,把第 \(r+1\) 个数转到 \(root\) 下,区间 \([l,r]\) 就在 \(r+1\) 点的左子树。

int root,rf,cnt,ch[N][2],pa[N],rev[N],val[N],sz[N];
void pushdown(int x){
	if(!rev[x]) return;
	swap(ch[x][0],ch[x][1]);
	rev[ch[x][0]]^=1; rev[ch[x][1]]^=1;
	rev[x]=0;
}
void update(int x){ sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]]; }
void rotate(int x,int ty){
	int fa=pa[x],son=ch[x][ty^1],gp=pa[fa];
	ch[fa][ty]=son; if(son) pa[son]=fa;
	ch[x][ty^1]=fa; pa[fa]=x;
	pa[x]=gp; ch[gp][fa==ch[gp][1]]=x;
	if(fa==root) root=x;
	update(fa); update(x);
}
void splay(int x,int t){
	while(pa[x]!=t){
		if(pa[pa[x]]==t) rotate(x,x==ch[pa[x]][1]);
		else{
			int fa=pa[x],gp=pa[fa];
			int f=(fa==ch[gp][0]);
			if(x==ch[fa][f]) rotate(x,f),rotate(x,!f);
			else rotate(fa,!f),rotate(x,!f);
		}
	}
}
int find(int x,int k){
	if(!x) return 0;
	pushdown(x);
	if(sz[ch[x][0]]>=k) return find(ch[x][0],k);
	if(sz[ch[x][0]]+1==k) return x;
	return find(ch[x][1],k-1-sz[ch[x][0]]);
}
void reverse(int l,int r){ //区间reverse
	int x=find(root,l-1),y=find(root,r+1);
	splay(x,rf); splay(y,root);
	rev[ch[y][0]]^=1;
}

可并堆(左偏树)

重点是每个点维护 \(dis\) 表示到最近的叶节点的距离
左偏树是棵二叉树,满足左叉的 \(dis\) 大于右叉;由此可知一个非叶子结点的 \(dis\) 等于右子树 \(dis+1\)
左偏树是个小根堆 \(or\) 大根堆,满足根节点是子树中的极值

两个堆合并时,先判断根节点应来自哪棵树,然后另一棵树与这棵树的右子树合并;合并完后注意维护 \(dis\) 并适当交换左右子树使满足左偏条件。
删除极值(根)时,把根的左右子树合并即可。
删除任意节点时,找到这个点,把它的左右子树合并,再一路更新至根。

有时候连通性用并查集维护

int ch[N][2],dis[N],val[N];
int merge(int x,int y){
	if(!x || !y) return x+y;
	if(val[x]>val[y] || (val[x]==val[y] && x>y)) swap(x,y);
	ch[x][1]=merge(ch[x][1],y);
	if(dis[ch[x][1]]>dis[ch[x][0]]) swap(ch[x][1],ch[x][0]);
	dis[x]=dis[ch[x][1]]+1;
	return x;
}

笛卡尔树

对序列建立的类似大根堆 \(or\) 小根堆,中序遍历是原序列
它可以告诉我们每个点是多大范围内的极值点
建立完成后就是树上问题了
经典问题是最大子矩形
对于部分题目也是很妙的入手点

\(O(n)\) 建树(栈维护最右侧的链)

int n,root,ch[N][2],val[N];
int st[N],top;
void build(){
    top=0;
    for(int i=1;i<=n;i++){
        while(top && val[st[top]]>val[i]) top--;
        ch[i][0]=ch[st[top]][1]; ch[st[top]][1]=i'
        st[++top]=i;
    }
    root=ch[0][1];
}

作用:
1.维护树链上的信息
2.维护最大 \(or\) 最小生成树:注意要化边为点
3.维护连通性
4.维护边双连通分量:发现环后,把环上所有点指向另一个点,相当于缩成了一个点(可用并查集维护)
5.维护原图(子树)信息:每个点新加一个附加值储存虚子树的贡献,每次虚实子树变化时(如 \(access\)\(link\))要更新这个值

无删除时连通性用并查集判断比较快
\(find\) 之后要加个 \(splay\),否则比较慢qwq

int rt[N],ch[N][2],pa[N],rev[N],sum[N],val[N];
void update(int x) { sum[x]=sum[ch[x][0]]^sum[ch[x][1]]^val[x]; }
void pushdown(int x){
	if(!rev[x]) return;
	swap(ch[x][0],ch[x][1]);
	rev[ch[x][0]]^=1; rev[ch[x][1]]^=1;
	rev[x]=0;
}
int st[N],top;
void push(int x){
	top=0; st[++top]=x;
	while(!rt[x]){
		x=pa[x];
		st[++top]=x;
	}
	while(top) pushdown(st[top--]);
}
void rotate(int x,int ty){
	int son=ch[x][ty^1],fa=pa[x],gp=pa[fa];
	ch[fa][ty]=son; if(son) pa[son]=fa;
	ch[x][ty^1]=fa; pa[fa]=x;
	pa[x]=gp;
	if(rt[fa]) rt[fa]=0,rt[x]=1;
	else ch[gp][fa==ch[gp][1]]=x;
	update(fa); update(x);
}
void splay(int x){
	push(x);
	while(!rt[x]){
		if(rt[pa[x]])
			rotate(x,x==ch[pa[x]][1]);
		else{
			int fa=pa[x],gp=pa[fa];
			int f=(fa==ch[gp][0]);
			if(x==ch[fa][f]) rotate(x,f),rotate(x,!f);
			else rotate(fa,!f),rotate(x,!f);
		}
	}
}

void access(int x){
	int y=0;
	while(x){
		splay(x);
		if(ch[x][1]) rt[ch[x][1]]=1; 
		rt[y]=0; ch[x][1]=y;
		update(x);
		y=x; x=pa[x];
	}
}
void makeroot(int x) { access(x); splay(x); rev[x]^=1; }
int find(int x){
	splay(x);
	while(ch[x][0]) x=ch[x][0];
	splay(x); return x;
}
void link(int x,int y) {
	if(find(x)==find(y)) return;
	makeroot(x); pa[x]=y;
}
void cut(int x,int y){
	makeroot(x); access(y); splay(y);
	if(ch[y][0]==x && !ch[x][1]){
		rt[x]=1; pa[x]=0;
		ch[y][0]=0;
		update(y);
	}
}

可持久化数据结构

可持久化线段树(主席树)

一般都是可持久化权值线段树
单点修改成一个新版本没啥,记得每次新建点,未新建的部分指向上一版本的结点
区间修改可以标记永久化,记得每次新建点
注意空间开够

可持久化trie

一般都是01trie,操作和主席树没什么区别
但用处很多,尤其是与二进制相关的,经典题求最大异或和

可持久化treap

\(fhq-treap\) 的基础上可持久化,\(merge\)\(split\) 是都新建节点比较保险,但实际由于 \(merge\) 基本都在 \(split\) 后进行,会被 \(merge\) 用到的结点已经是该版本新建的了,就不用再建了。

int cnt,ch[N*20][2],val[N*20],rnd[N*20],rt[N],sz[N*20];
void update(int x) { sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]]; }
int merge(int x,int y){
	if(!x || !y) return x+y;
	if(rnd[x]>rnd[y]){
		ch[x][1]=merge(ch[x][1],y);
		update(x); return x;
	}
	else{
		ch[y][0]=merge(x,ch[y][0]);
		update(y); return y;
	}
}
Pr split(int x,int k){//split by size
	if(!x) return Pr(0,0);
	int z=++cnt;
	val[z]=val[x]; rnd[z]=rnd[x];
	if(sz[ch[x][0]]>=k){
		Pr w=split(ch[x][0],k);
		ch[z][1]=ch[x][1]; ch[z][0]=w.se;
		update(z); return Pr(w.fi,z);
	}
	else{
		Pr w=split(ch[x][1],k-sz[ch[x][0]]-1);
		ch[z][0]=ch[x][0]; ch[z][1]=w.fi;
		update(z); return Pr(z,w.se);
	}
}

可持久化可并堆

可持久化并查集


树套树

二维数点

静态离线:扫描线,一维排序、另一位树状数组维护
静态在线:主席树
动态离线:CDQ分治
动态在线:范围小的可以二维树状数组,范围大的树状数组套主席树

三维偏序(树状数组套treap)

因为三维偏序没有区间内第 \(k\) 大类似的操作,所以用树状数组差分就可以了

int cnt,root[N],ch[N*20][2],sz[N*20],val[N*20],rnd[N*20];
void update(int x) { sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]]; }
int merge(int x,int y){
	if(!x || !y) return x+y;
	if(rnd[x]>rnd[y]){
		ch[x][1]=merge(ch[x][1],y);
		update(x); return x;
	}
	else{
		ch[y][0]=merge(x,ch[y][0]);
		update(y); return y;
	}
}
Pr split(int x,int k){
	if(!x) return Pr(0,0);
	Pr z;
	if(val[x]<=k) {
		z=split(ch[x][1],k);
		ch[x][1]=z.fi; update(x);
		return Pr(x,z.se);
	}
	else{
		z=split(ch[x][0],k);
		ch[x][0]=z.se; update(x);
		return Pr(z.fi,x);
	}
}
int sml(int id,int k){
	Pr z=split(root[id],k);
	int ret=sz[z.fi];
	root[id]=merge(z.fi,z.se);
	return ret;
}

int n,k;
void add(int x,int y){
	while(x<=k){
		int tmp=++cnt;
		val[tmp]=y; sz[tmp]=1; rnd[tmp]=getrnd();
		Pr z=split(root[x],y);
		root[x]=merge(z.fi,merge(tmp,z.se));
		
		x+=x&(-x);
	}
}
int sum(int x,int y){
	int ret=0;
	while(x){
		ret+=sml(x,y);
		x-=x&(-x);
	}
	return ret;
}

线段树套线段树

有可能空间爆炸,那样就需要牺牲时间换成 \(KD-Tree\)
当然也可以离线

树状数组套主席树(线段树)

相当于可修改历史版本的主席树
用树状数组动态维护前缀。

线段树套平衡树

不维护减的话只能用线段树了
模板题二逼平衡树,写的是线段树套 \(FHQtreap\)

#include<cstdio>
#include<iostream>
#include<algorithm>

#define fi first
#define se second
#define INF 1000000000

using namespace std;

int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch) && ch!='-') ch=getchar();
	if(ch=='-') f=-1,ch=getchar();
	while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
	return x*f;
}

const int N = 50005;
typedef pair<int,int> Pr;

int rr;
int getrnd() { return rr=1ll*rr*rr%4987657+1ll*rr*961275%39916801; } //rr=!!!

int n,m,tot;
int root[N*2],ch[N*20][2],val[N*20],rnd[N*20],sz[N*20];
void update(int x) { sz[x]=1+sz[ch[x][0]]+sz[ch[x][1]]; }
int merge(int x,int y){
	if(!x || !y) return x+y;
	if(rnd[x]>rnd[y]){
		ch[x][1]=merge(ch[x][1],y);
		update(x); return x;
	}
	else{
		ch[y][0]=merge(x,ch[y][0]);
		update(y); return y;
	}
}
Pr split(int x,int k){ //by val
	if(!x) return Pr(0,0);
	Pr z;
	if(val[x]<=k){
		z=split(ch[x][1],k);
		ch[x][1]=z.fi; update(x);
		return Pr(x,z.se);
	}
	else{
		z=split(ch[x][0],k);
		ch[x][0]=z.se; update(x);
		return Pr(z.fi,x);
	}
}
int find(int x,int k){
	if(!x) return 0;
	if(sz[ch[x][0]]>=k) return find(ch[x][0],k);
	if(sz[ch[x][0]]+1==k) return x;
	return find(ch[x][1],k-sz[ch[x][0]]-1);
}
int pre(int id,int k){
	Pr z=split(root[id],k-1);
	int x=find(z.fi,sz[z.fi]);
	root[id]=merge(z.fi,z.se); 
	return x?val[x]:-INF; //x?
}
int sub(int id,int k){
	Pr z=split(root[id],k);
	int x=find(z.se,1);
	root[id]=merge(z.fi,z.se);
	return x?val[x]:INF; //x?
}
int pos[N],bb[N];
void modify(int id,int x,int y){ //from x->y
	Pr w=split(root[id],x),t=split(w.fi,x-1);
	int tmp=t.se; 
	root[id]=merge(merge(t.fi,merge(ch[tmp][0],ch[tmp][1])),w.se);
	val[tmp]=y; sz[tmp]=1; ch[tmp][0]=ch[tmp][1]=0;
	w=split(root[id],y-1);
	root[id]=merge(merge(w.fi,tmp),w.se);
}
int num(int x,int k){
	if(!x) return 0;
	if(val[x]<=k) return sz[ch[x][0]]+1+num(ch[x][1],k);
	return num(ch[x][0],k);
}

int cnt,rt,son[N*2][2];
void build(int x,int l,int r){
	root[x]=0;
	for(int i=l;i<=r;i++) bb[i-l+1]=pos[i];
	sort(bb+1,bb+1+r-l+1);
	for(int i=1;i<=r-l+1;i++){
		int tmp=++tot; 
		val[tmp]=bb[i]; sz[tmp]=1; rnd[tmp]=getrnd();
		root[x]=merge(root[x],tmp);
	}
	
	if(l==r) return;
	int mid=(l+r)>>1;
	build(son[x][0]=++cnt,l,mid);
	build(son[x][1]=++cnt,mid+1,r);
}
int Pre(int x,int l,int r,int L,int R,int c){
	if(L<=l && r<=R) return pre(x,c);
	int mid=(l+r)>>1,ret=-INF;
	if(L<=mid) ret=max(ret,Pre(son[x][0],l,mid,L,R,c));
	if(R>mid) ret=max(ret,Pre(son[x][1],mid+1,r,L,R,c));
	return ret;
}
int Sub(int x,int l,int r,int L,int R,int c){
	if(L<=l && r<=R) return sub(x,c);
	int mid=(l+r)>>1,ret=INF;
	if(L<=mid) ret=min(ret,Sub(son[x][0],l,mid,L,R,c));
	if(R>mid) ret=min(ret,Sub(son[x][1],mid+1,r,L,R,c));
	return ret;
}
void Modify(int x,int l,int r,int c,int fr,int to){
	modify(x,fr,to);
	if(l==r) return;
	int mid=(l+r)>>1;
	if(c<=mid) Modify(son[x][0],l,mid,c,fr,to);
	else Modify(son[x][1],mid+1,r,c,fr,to);
}
int rk(int x,int l,int r,int L,int R,int c){
	if(L<=l && r<=R) return num(root[x],c-1);
	int mid=(l+r)>>1,ret=0;
	if(L<=mid) ret+=rk(son[x][0],l,mid,L,R,c);
	if(R>mid) ret+=rk(son[x][1],mid+1,r,L,R,c);
	return ret;
}

int kth(int L,int R,int k){
	int l=-INF,r=INF,mid;
	while(l<r){
		mid=(l+r+1)>>1;
		if(rk(rt,1,n,L,R,mid)<k) l=mid;
		else r=mid-1;
	}
	return l;
}

int main()
{
	rr=1;
	n=read(); m=read();
	for(int i=1;i<=n;i++) pos[i]=read();
	build(rt=++cnt,1,n);
	int ty,l,r,x;
	while(m--){
		ty=read(); 
		if(ty==3){
			l=read(); x=read();
			Modify(rt,1,n,l,pos[l],x);
			pos[l]=x;
		}
		else{
			l=read(); r=read(); x=read();
			if(ty==1) printf("%d\n",rk(rt,1,n,l,r,x)+1);
			else if(ty==2) printf("%d\n",kth(l,r,x));
			else if(ty==4) printf("%d\n",Pre(rt,1,n,l,r,x));
			else printf("%d\n",Sub(rt,1,n,l,r,x));
		}
	}
	
	return 0;
}

KD-Tree

(反正是解决高维问题,就算在这里吧)


离线做法

CDQ分治

整体二分

线段树分治


分块


莫队

posted @ 2020-08-03 21:13  秋千旁的蜂蝶~  阅读(37)  评论(0编辑  收藏