cunzai_zsy0531

关注我

数据结构学习笔记

Post time: 2021-04-06 18:41:58

说在前面

省选前攒一波 \(rp\),冲几发数据结构吧……

update 2021.4.12:SDOI2021黑了…… rp--可还行。

LCT

\(Link-Cut\ Tree\),动态树,可以动态维护一棵树或者一个森林。主要思想是实链剖分,Splay维护森林,对于每一条链维护一些值。

几个需要注意的地方:

  1. 码风,尽量在所有函数里用 \(x\) 表示当前节点。
  2. rotate和splay不一样的地方在于,需要考虑特殊情况。一定要在保证可行的前提下再操作,并且在整个操作过程中一定要保证下标为 \(0\) 的点不会被修改!
  3. splay里边用的栈要手写,不要用stl,太慢了。注意边界的修改。
  4. 写之前先想好要求的东西能不能用LCT中的refresh维护。

最近写的一波板子:

点击查看代码
struct Link_Cut_Tree{
	struct Stack{
		int s[N],t;
		inline void clear(){t=0;}
		Stack(){clear();}
		inline void push(int x){s[++t]=x;}
		inline int top(){return s[t];}
		inline void pop(){--t;}
		inline bool empty(){return !t;}
	};
	int fa[N],val[N],ch[N][2];bool tag[N];
	inline void refresh(int x){val[x]=a[x]^val[ch[x][0]]^val[ch[x][1]];}
	inline bool isroot(int x){return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;}
	inline bool chk(int x){return ch[fa[x]][1]==x;}
	inline void rotate(int x){
		int f=fa[x],gf=fa[f],k=chk(x),w=ch[x][k^1];
		fa[x]=gf;if(!isroot(f)) ch[gf][chk(f)]=x;
		if(w) fa[w]=f;ch[f][k]=w;
		fa[f]=x;ch[x][k^1]=f;
		refresh(f),refresh(x);
	}
	inline void pushdown(int x){
		if(!tag[x]) return;
		tag[ch[x][0]]^=1,tag[ch[x][1]]^=1,tag[x]=0;
		swap(ch[x][0],ch[x][1]);
	}
	inline void splay(int x){
		Stack st;
		int p=x;
		while(!isroot(p)) st.push(p),p=fa[p];
		st.push(p);
		while(!st.empty()) pushdown(st.top()),st.pop();
		while(!isroot(x)){
			int f=fa[x],gf=fa[f];
			if(!isroot(f)){
				if(chk(f)==chk(x)) rotate(f);
				else rotate(x);
			}
			rotate(x);
		}
	}
	inline void access(int x){
		for(int p=0;x;p=x,x=fa[x]) splay(x),ch[x][1]=p,refresh(x);
	}
	inline void makeroot(int x){
		access(x);
		splay(x);
		tag[x]^=1;
	}
	inline int findroot(int x){
		access(x);
		splay(x);
		while(ch[x][0]) x=ch[x][0];
		return x;
	}
	inline void split(int x,int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	inline void link(int x,int y){
		makeroot(x);
		if(findroot(y)!=x) fa[x]=y;
	}
	inline void cut(int x,int y){
		split(x,y);
		if(ch[y][0]==x&&!ch[x][1]) fa[x]=ch[y][0]=0;
	}
	inline void modify(int x,int y){
		access(x);
		splay(x);
		a[x]=y,refresh(x);
	}
}T;

UPDATE 2021.5.13:然后LCT因为十级不考了……

虚树

名字听起来高大上,但实际上没那么难。虚树用来解决树上关键点问题。如果有 \(q\) 组询问,暴力做法是 \(O(qn)\)(或者再有一个 \(\log\)),而且所有询问实际上发挥作用的点的总数的和是 \(O(n)\) 的,那么就可以用虚树来解决。

注意建立虚树过程中,用栈来记录最右链,这条链左端(左边就是 \(dfs\) 序更小的子树)的所有关键点已经被匹配了。这样就可以通过分类讨论来 \(O(k)\) 建树。建树过程中注意会调用栈顶的下面那个元素,考虑边界的时候不要出错。

建出来虚树之后基本上就和暴力做法一样了,可能还需要多在原树上预处理一点东西即可。

板子:

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
inline ll rd(){
	ll res=0,flag=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') flag=-1;c=getchar();}
	while(c>='0'&&c<='9') res=(res<<1)+(res<<3)+(c-'0'),c=getchar();
	return res*flag;
}
void wt(ll x){
	if(x>9) wt(x/10);
	putchar(x%10+'0');
}
inline ll min(const ll &a,const ll &b){return a<b?a:b;}
const int N=5e5+13;
const ll INF=0x3f3f3f3f3f3f3f3fll;
struct Stack{
	int s[N],t;
	inline void clear(){s[t=0]=0;}
	Stack(){clear();}
	inline void push(int x){s[++t]=x;}
	inline void pop(){--t;}
	inline int ttop(){return s[t-1];}
	inline int top(){return s[t];}
	inline bool empty(){return !t;}
};
struct Edge{int v,w,nxt;}e[N<<1];
int n,m,k,tot,h[N],fa[N],siz[N],dep[N],son[N],top[N],id[N],a[N],num;
bool vis[N];
ll f[N],minw[N];
inline void add(int u,int v,int w){e[++tot]=(Edge){v,w,h[u]};h[u]=tot;}
void dfs1(int u,int f,int deep){
	fa[u]=f,siz[u]=1,dep[u]=deep;
	int maxson=0;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v,w=e[i].w;if(v==f) continue;
		minw[v]=min(minw[u],w);
		dfs1(v,u,deep+1);
		siz[u]+=siz[v];
		if(siz[v]>maxson) maxson=siz[v],son[u]=v;
	}
}
void dfs2(int u,int topf){
	top[u]=topf,id[u]=++num;
	if(!son[u]) return;
	dfs2(son[u],topf);
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v!=son[u]&&v!=fa[u]) dfs2(v,v);
	}
}
inline int lca(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		u=fa[top[u]];
	}
	return id[u]<id[v]?u:v;
}
Edge E[N];int H[N],Tot;
inline bool cmp(const int &x,const int &y){return id[x]<id[y];}
inline void Add(int u,int v){E[++Tot]=(Edge){v,0,H[u]};H[u]=Tot;}
Stack st;
inline void build(){
	st.clear();Tot=0;
	sort(a+1,a+k+1,cmp);
	st.push(1);
	for(int i=1;i<=k;++i){
		int t=lca(a[i],st.top());
		if(t!=st.top()){
			while(st.t>1&&id[t]<id[st.ttop()]) Add(st.ttop(),st.top()),st.pop();
			if(t!=st.ttop()) Add(t,st.top()),st.pop(),st.push(t);
			else Add(t,st.top()),st.pop();
		}
		st.push(a[i]);
	}
	for(int i=1;i<st.t;++i) Add(st.s[i],st.s[i+1]);
}
void init(int u){
	f[u]=minw[u];
	ll sum=0;
	for(int i=H[u];i;i=E[i].nxt){
		int v=E[i].v;
		init(v);sum+=f[v];
	}
	if(!vis[u]) f[u]=min(f[u],sum);
	H[u]=0;
}
int main(){
	n=rd();
	for(int i=1,u,v,w;i<n;++i) u=rd(),v=rd(),w=rd(),add(u,v,w),add(v,u,w);
	minw[1]=INF;dfs1(1,0,0),dfs2(1,1);
	m=rd();
	for(int i=1;i<=m;++i){
		k=rd();
		for(int j=1;j<=k;++j) a[j]=rd(),vis[a[j]]=1;
		build();
		init(1);
		for(int j=1;j<=k;++j) vis[a[j]]=0;
		wt(f[1]);putchar('\n');
	}
	return 0;
}

区间最值 & 历史最值线段树(吉老师线段树)segmenttree beats

区间最值,就是带这样操作的数据结构题:

1 x y 表示把 \(a_x\) 变为 \(\max(\min) (a_x,y)\)

这个东西看起来很难处理,于是我们需要重新思考最初学习线段树的时候,对于lazy-tag的理解。

lazy-tag是什么?它的意义其实是一些操作的并。如果有很多个加操作在这个区间,那么可以在这里把这些操作合并,当需要用到这个区间的子区间时,再一起下传。因为如果要把所有操作都记下来复杂度显然很高,这个lazy-tag的存在保证了复杂度。

假设这个操作是 \(\max(a_x,y)\),然后最后要求区间 \(\min\)。那么,可以维护一个区间最小值 \(mi\),区间内值等于这个最小值的数的个数 \(c\),以及区间次小值 \(se\)。这样的话,每次到一个节点,假设现在要区间取 \(\max\) 的数为 \(x\),那么考虑这个值和 \(mi,se\) 的大小关系。显然,如果 \(x\leq mi\),那么一定不会再有任何修改和贡献;如果 \(mi<x< se\),那么只需要更新 \(mi\) 的值,通过个数 \(c\) 来改区间其他值(比如区间和等);如果 \(x\geq se\),则继续下传。这个东西的复杂度看似很高但却不高,一个比较简单的证明是,考虑每次暴力dfs的时候,搜索到的区间不同的值的个数。每次下传左右孩子至少有一个把最大值和次大值合并,所以说这个区间不同值的个数会减少,那么 \(\log n\) 层,每层最多是 \(O(n)\),则复杂度可以证明一个下界是 \(O((n+m)\log n)\) 的。如果加上了加法操作,由于每次加法操作会修改 \(O(\log n)\) 个点的值,所以总的复杂度多一个 \(\log\)

历史最值的核心在于设一个 \(preadd\) 表示每个点在上一次下传之后的加法标记最大值。利用了lazy-tag合并操作的性质,合并历史最大标记之后,在pushdown的过程中可以直接用它来统计历史最大值,总的复杂度是 \(O((n+m)\log n\)

另外,区间最值和历史最值相结合的时候,将数分成最大值和非最大值两类分别维护加法标记和历史最大加法标记,这样的复杂度在之前已经证明是 \(O(m\log^2 n)\)。这个做法看似很暴力,但是它能够启发我们在一类统计区间问题很困难时,从值域入手,对特殊值(最值)分治,通过线段树一类数据结构的优秀性质,仅仅花费 \(1\)\(2\)\(\log\) 转化为只需要考虑特殊值的影响,就可以比较简单和暴力地做出来。

写代码的时候注意变量的可读性以及小细节的错误,出现问题之后先默读一遍代码看看有没有细节上很显然的问题。

模板P6242

点击查看代码
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
inline int max(const int &a,const int &b){return a>b?a:b;}
inline int min(const int &a,const int &b){return a<b?a:b;}
inline int rd(){
	int res=0,flag=1;char c=getchar();
	for(;!isdigit(c);c=getchar())if(c=='-')flag=-1;
	for(;isdigit(c);c=getchar())res=(res<<1)+(res<<3)+(c-'0');
	return res*flag;
}
void wt(ll x){if(x>9)wt(x/10);putchar(x%10+'0');}
const int N=5e5+13,INF=0x3f3f3f3f;
struct SegTree{int l,r,maxx,premax,se,cnt,add,preadd,addmax,preaddmax;ll sum;}t[N<<2];
int n,m;
#define ls p<<1
#define rs p<<1|1
#define mid ((t[p].l+t[p].r)>>1)
inline void refresh(int p){
	t[p].sum=t[ls].sum+t[rs].sum;
	t[p].premax=max(t[ls].premax,t[rs].premax);
	if(t[ls].maxx>t[rs].maxx) t[p].maxx=t[ls].maxx,t[p].cnt=t[ls].cnt,t[p].se=max(t[ls].se,t[rs].maxx);
	else if(t[ls].maxx==t[rs].maxx) t[p].maxx=t[ls].maxx,t[p].cnt=t[ls].cnt+t[rs].cnt,t[p].se=max(t[ls].se,t[rs].se);
	else t[p].maxx=t[rs].maxx,t[p].cnt=t[rs].cnt,t[p].se=max(t[ls].maxx,t[rs].se);
}
void build(int p,int l,int r){
	t[p].l=l,t[p].r=r;
	if(l==r){t[p].sum=t[p].maxx=t[p].premax=rd(),t[p].se=-INF,t[p].cnt=1;return;}
	build(ls,l,mid);build(rs,mid+1,r);
	refresh(p);
}
inline void pushup(int p,int x,int px,int x_m,int px_m){
	t[p].sum+=1ll*t[p].cnt*x_m+1ll*(t[p].r-t[p].l+1-t[p].cnt)*x;
	t[p].premax=max(t[p].premax,t[p].maxx+px_m);
	t[p].preadd=max(t[p].preadd,t[p].add+px);
	t[p].preaddmax=max(t[p].preaddmax,t[p].addmax+px_m);
	t[p].add+=x,t[p].addmax+=x_m,t[p].maxx+=x_m;
	if(t[p].se!=-INF) t[p].se+=x;
}
inline void pushdown(int p){
	int tmp=max(t[ls].maxx,t[rs].maxx);
	if(t[ls].maxx==tmp) pushup(ls,t[p].add,t[p].preadd,t[p].addmax,t[p].preaddmax);
	else pushup(ls,t[p].add,t[p].preadd,t[p].add,t[p].preadd);
	if(t[rs].maxx==tmp) pushup(rs,t[p].add,t[p].preadd,t[p].addmax,t[p].preaddmax);
	else pushup(rs,t[p].add,t[p].preadd,t[p].add,t[p].preadd);
	t[p].add=t[p].preadd=t[p].addmax=t[p].preaddmax=0;
}
void update(int p,int l,int r,int x){
	if(l<=t[p].l&&t[p].r<=r) return pushup(p,x,x,x,x);
	pushdown(p);
	if(l<=mid) update(ls,l,r,x);
	if(r>mid) update(rs,l,r,x);
	refresh(p);
}
void modify(int p,int l,int r,int x){
	if(x>=t[p].maxx) return;
	if(l<=t[p].l&&t[p].r<=r&&x>t[p].se) return pushup(p,0,0,x-t[p].maxx,x-t[p].maxx);
	pushdown(p);
	if(l<=mid) modify(ls,l,r,x);
	if(r>mid) modify(rs,l,r,x);
	refresh(p);
}
ll query_sum(int p,int l,int r){
	if(l<=t[p].l&&t[p].r<=r) return t[p].sum;
	pushdown(p);ll res=0;
	if(l<=mid) res+=query_sum(ls,l,r);
	if(r>mid) res+=query_sum(rs,l,r);
	return res;
}
int query_max(int p,int l,int r){
	if(l<=t[p].l&&t[p].r<=r) return t[p].maxx;
	pushdown(p);int res=-INF;
	if(l<=mid) res=max(res,query_max(ls,l,r));
	if(r>mid) res=max(res,query_max(rs,l,r));
	return res;
}
int query_premax(int p,int l,int r){
	if(l<=t[p].l&&t[p].r<=r) return t[p].premax;
	pushdown(p);int res=-INF;
	if(l<=mid) res=max(res,query_premax(ls,l,r));
	if(r>mid) res=max(res,query_premax(rs,l,r));
	return res;
}
inline void file(){
	freopen("P6242_1.in","r",stdin);
	freopen("P6242.out","w",stdout);
}
int main(){
	//file();
	n=rd(),m=rd();
	build(1,1,n);
	while(m--){
		int op,l,r,x;
		op=rd(),l=rd(),r=rd();
		switch(op){
			case 1:x=rd();update(1,l,r,x);break;
			case 2:x=rd();modify(1,l,r,x);break;
			case 3:printf("%lld\n",query_sum(1,l,r));break;
			case 4:printf("%d\n",query_max(1,l,r));break;
			case 5:printf("%d\n",query_premax(1,l,r));break;
		}
	}
	return 0;
}

李超线段树

李超树是一种用来维护线段的线段树。考虑如果说有 \(m\) 个线段加入,并且有很多次查询某个 \(x\) 坐标上的最大线段,无法用普通的线段树来做,这时候就需要李超树。李超树的核心思想在于,记录每个区间的当前“最优”线段,最后因为只有单点查询,所以直接统计线段树上这个点到根的路径上所有线段的最优值即可。

考虑现在加入一个线段 \(u\),当前节点的最优线段编号为 \(v\),那么分类讨论:

首先对斜率大小进行讨论:

\(u.k=v.k\),那么只需要比较两条线段在 \(mid\) 处的值大小,大的那条线段在任何位置都一定更优,可以直接退出。

\(u.k>v.k\),比较两条线段在 \(mid\) 处的取值 \(val(u,mid)\)\(val(v,mid)\)

  1. \(val(u,mid)>val(v,mid)\)\(u\) 在右边(即\((mid+1,r)\))一定更优,\(v\) 在左边可能更优;
  2. \(val(u,mid)<val(v,mid)\)\(v\) 在左边一定更优,\(u\) 在右边可能更优。

\(u.k<v.k\),仍然比较两条线段在 \(mid\) 处的取值 \(val(u,mid)\)\(val(v,mid)\)

  1. \(val(u,mid)>val(v,mid)\)\(u\) 在左边一定更优,\(v\) 在右边可能更优;
  2. \(val(u,mid)<val(v,mid)\)\(v\) 在右边一定更优,\(u\) 在左边可能更优。

所有 \(val(u,mid)=val(v,mid)\) 的情况都可以被归到两种情况中的任何一种。

现在已经讨论完了所有情况,考虑如何实现 \(update\) 一条直线。可以发现,如果在当前节点表示的区间,最优线段是 \(u\),那么这个线段就不需要再下传了。只有当它被一条更优的线段取代的时候,通过上边的分析,如果说它在下面的某个区间还可能更优的话,才会被下传。所以总共的修改复杂度为 \(O(\log^2 n)\),询问复杂度为 \(O(\log n)\)

模板P4097

点击查看代码
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int N=40000+13,M=1e5+13;
const double eps=1e-10;
struct Segment{double k,b;}a[M];
struct Node{
	int id;double y;
	bool operator<(const Node &a)const{
		if(fabs(y-a.y)<eps) return id>a.id;
		return y<a.y;
	}
};
int n=39989,m,lim=1e9,cnt;
inline int add(int x0,int y0,int x1,int y1){
	++cnt;
	if(x0==x1) a[cnt].k=0,a[cnt].b=max(y0,y1);
	else{
		a[cnt].k=1.0*(y1-y0)/(x1-x0);
		a[cnt].b=y0-a[cnt].k*x0;
	}
	return cnt;
}
inline double val(int id,int x){return a[id].k*x+a[id].b;}
struct SegTree{int l,r,id;}t[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((t[p].l+t[p].r)>>1)
void build(int p,int l,int r){
	t[p].l=l,t[p].r=r;
	if(l==r) return;
	build(ls,l,mid);build(rs,mid+1,r);
}
void update(int p,int l,int r,int id){
	if(l<=t[p].l&&t[p].r<=r){
		if(!t[p].id){t[p].id=id;return;}
		if(t[p].l==t[p].r){
			if(val(id,t[p].l)>val(t[p].id,t[p].l)) t[p].id=id;
			return;
		}
		if(fabs(a[id].k-a[t[p].id].k)<eps){
			if(val(id,mid)>val(t[p].id,mid)) t[p].id=id;
		}
		else if(a[id].k>a[t[p].id].k){
			if(val(id,mid)>val(t[p].id,mid)){
				update(ls,l,r,t[p].id);
				t[p].id=id;
			}
			else update(rs,l,r,id);
		}
		else{
			if(val(id,mid)>val(t[p].id,mid)){
				update(rs,l,r,t[p].id);
				t[p].id=id;
			}
			else update(ls,l,r,id);
		}
		return;
	}
	if(l<=mid) update(ls,l,r,id);
	if(r>mid) update(rs,l,r,id);
}
Node query(int p,int x){
	Node res=(Node){t[p].id,val(t[p].id,x)};
	if(t[p].l==t[p].r) return res;
	return max(res,x<=mid?query(ls,x):query(rs,x));
}
int main(){
	//freopen("P4097_1.in.txt","r",stdin);
	//freopen("P4097.out","w",stdout);
	build(1,1,n);
	scanf("%d",&m);int lastans=0;
	while(m--){
		int op,x0,y0,x1,y1,x;
		scanf("%d",&op);
		if(op){
			scanf("%d%d%d%d",&x0,&y0,&x1,&y1);
			x0=(x0+lastans-1)%n+1,x1=(x1+lastans-1)%n+1;
			y0=(y0+lastans-1)%lim+1,y1=(y1+lastans-1)%lim+1;
			if(x0>x1) swap(x0,x1),swap(y0,y1);
			int id=add(x0,y0,x1,y1);
			update(1,x0,x1,id);
		}
		else{
			scanf("%d",&x);x=(x+lastans-1)%n+1;
			Node ans=query(1,x);
			printf("%d\n",(lastans=ans.id));
		}
	}
	return 0;
}

线段树分治

线段树分治是一个解决在线算法不够优秀的离线做法。首先在时间轴上建立线段树,然后把所有修改放到线段树的 \(O(\log n)\) 个区间内,最后dfs线段树一遍求出叶子节点的答案。注意这里的修改操作必须支持可撤销,比如并查集只能按秩合并,并且要用栈来支持可撤销。

板子题大概是说,需要判断一个图是二分图当且仅当其没有奇环。奇环的求法就是扩展域并查集。

模板P5787

点击查看代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<stack>
using namespace std;
inline int rd(){
	int res=0;char c=getchar();
	for(;!isdigit(c);c=getchar());
	for(;isdigit(c);c=getchar())res=(res<<1)+(res<<3)+(c-'0');
	return res;
}
const int N=2e5+13;
int n,m,k;bool ans[N];
struct edge{int u,v;}E[N];
struct SegTree{int l,r;vector<int>e;}t[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid ((t[p].l+t[p].r)>>1)
void build(int p,int l,int r){
	t[p].l=l,t[p].r=r;
	if(l==r) return;
	build(ls,l,mid),build(rs,mid+1,r);
}
void update(int p,int l,int r,int x){
	if(l<=t[p].l&&t[p].r<=r) return t[p].e.push_back(x);
	if(l<=mid) update(ls,l,r,x);
	if(r>mid) update(rs,l,r,x);
}
struct Node{int x,y,k;};
stack<Node> s;
int fa[N],dep[N];
int find(int x){return fa[x]==x?x:find(fa[x]);}
inline void merge(int u,int v){
	int x=find(u),y=find(v);
	if(x==y) return;
	if(dep[x]>dep[y]) swap(x,y);
	fa[x]=y,dep[y]+=(dep[x]==dep[y]);
	s.push((Node){x,y,dep[x]==dep[y]});
}
inline bool check(int u,int v){return find(u)==find(v);}
void dfs(int p){
	int lim=t[p].e.size(),tmp=s.size();bool flag=1;
	for(int i=0;i<lim;++i){
		int u=E[t[p].e[i]].u,v=E[t[p].e[i]].v;
		if(check(u,v)){flag=0;break;}
		merge(u,v+n),merge(v,u+n);
	}
	if(flag){
		if(t[p].l==t[p].r) ans[t[p].l]=1;
		else dfs(ls),dfs(rs);
	}
	lim=s.size();
	while(lim>tmp){
		fa[s.top().x]=s.top().x,dep[s.top().y]-=s.top().k;
		s.pop();--lim;
	}
}
int main(){
	n=rd(),m=rd(),k=rd();
	build(1,1,k);
	for(int i=1;i<=m;++i){
		E[i].u=rd(),E[i].v=rd();
		int l=rd(),r=rd();
		if(l<r) update(1,l+1,r,i);
	}
	for(int i=1;i<=(n<<1);++i) fa[i]=i,dep[i]=1;
	dfs(1);
	for(int i=1;i<=k;++i) puts(ans[i]?"Yes":"No");
	return 0;
}

笛卡尔树

笛卡尔树是一种二叉搜索树,每个点有两个权值 \((x_i,y_i)\),满足两个条件:

  1. 所有的 \(x_i\) 形成一棵二叉搜索树。
  2. 所有的 \(y_i\) 形成一个小根堆。

一般的建立是先使 \(x_i\) 单调增,这样每次插入的点如果是原来的点的孩子,那一定是右儿子;如果原来的点是这个点的孩子,那么一定是左儿子。然后就有一个 \(O(n)\) 建立笛卡尔树的方法:

拿单调栈存储当前树的最右链(一直走右儿子的链),那么每次一个点进来,一定是在这条链上找到深度最大的 \(y<y_i\) 的位置,接在这个点底下。然后原来在底下的那些点都接到这个点的左子树内,最后把这个点入栈。因为每个点只会入栈出栈一次,所以总的时间复杂度为 \(O(n)\)

模板P5854

点击查看代码
#include<iostream>
#include<cstdio>
#define rint register int
using namespace std;
inline int rd(){
	int res=0;char c=getchar();
	for(;!isdigit(c);c=getchar());
	for(;isdigit(c);c=getchar())res=(res<<1)+(res<<3)+(c-'0');
	return res;
}
const int N=1e7+13;
int n,a[N],s[N],top,L[N],R[N];
int main(){
	n=rd();
	for(rint i=1;i<=n;++i){
		a[i]=rd();rint pos=top;
		while(pos&&a[s[pos]]>a[i]) --pos;
		if(pos) R[s[pos]]=i;
		if(pos<top) L[i]=s[pos+1];
		s[top=(++pos)]=i;
	}
	long long ans1=0,ans2=0;
	for(rint i=1;i<=n;++i) ans1^=1ll*i*(L[i]+1),ans2^=1ll*i*(R[i]+1);
	printf("%lld %lld\n",ans1,ans2);
	return 0;
}

点分治

点分治可以解决一类树上的问题。板子题说的是问一棵边有权的树上有没有距离为 \(k\) 的点对。

做法有两种:第一种是,枚举这个距离的中间点,直接在子树中匹配。具体做法是把子树中所有的 \(d\) 都加到一个数据结构里,后面的每个 \(d\) 进来的时候,在数据结构里查找有没有值为 \(k-d\) 并且不在同一个子树内的。如果有那么这个询问的答案就可以。这个做法使用桶可以做到 \(O(nm\log n)\)

但是,如果不能使用桶(数据范围很大),可能就需要使用平衡树维护。第二种做法是一个不错的解决方式:把子树内所有点都放到一个数组里,按照 \(d\) 从小到大排序,然后定义两个指针 \(l,r\) 分别从头和尾来扫。注意到因为 \(l\) 单调递增,那么 \(r\) 只会单调不减,所以扫一遍的复杂度是 \(O(n)\) 的。

等等,是不是有什么问题?树是一条链的话,复杂度是不是假了?

怎么解决这个问题呢?考虑每次进入一个子树的时候,先找到子树的重心,这样每一次往下递归都会减少至少 \(\frac12\),所以总共需要上面那样扫的次数是 \(O(\log n)\) 的,算上排序,总共的复杂度是 \(O(n\log^2 n+nm\log n)\)

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
inline int max(const int &a,const int &b){return a>b?a:b;}
const int N=1e4+13;
struct Edge{int v,w,nxt;}e[N<<1];
int n,m,q[N],tot,cnt,h[N],rt,siz[N],maxson[N],a[N],b[N],d[N];
bool vis[N],ans[N];
inline bool cmp(const int &x,const int &y){return d[x]<d[y];}
inline void add(int u,int v,int w){e[++tot]=(Edge){v,w,h[u]};h[u]=tot;}
void getroot(int u,int fa,int total){
	siz[u]=1,maxson[u]=0;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(v==fa||vis[v]) continue;
		getroot(v,u,total);
		siz[u]+=siz[v];
		maxson[u]=max(maxson[u],siz[v]);
	}
	maxson[u]=max(maxson[u],total-siz[u]);
	if(!rt||maxson[u]<maxson[rt]) rt=u;
}
void init(int u,int fa,int sum,int top){
	a[++cnt]=u,d[u]=sum,b[u]=top;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(v==fa||vis[v]) continue;
		init(v,u,sum+e[i].w,top);
	}
}
void solve(int u){
	vis[u]=1;a[cnt=1]=u,b[u]=u,d[u]=0;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(vis[v]) continue;
		init(v,0,e[i].w,v);
	}
	sort(a+1,a+cnt+1,cmp);
	for(int i=1;i<=m;++i){
		if(ans[i]) continue;
		int l=1,r=cnt;
		while(l<r){
			if(d[a[l]]+d[a[r]]>q[i]) --r;
			else if(d[a[l]]+d[a[r]]<q[i]) ++l;
			else if(b[a[l]]==b[a[r]]){
				if(d[a[r-1]]==d[a[r]]) --r;
				else ++l;
			}
			else{ans[i]=1;break;}
		}
	}
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(vis[v]) continue;
		rt=0,getroot(v,0,siz[v]);getroot(rt,0,siz[rt]);
		solve(rt);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<n;++i) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w);
	for(int i=1;i<=m;++i) scanf("%d",&q[i]);
	getroot(1,0,n);
	solve(rt);
	for(int i=1;i<=m;++i) puts(ans[i]?"AYE":"NAY");
	return 0;
}

动态点分治(点分树)

考虑把点分治的过程建成树,这棵树的性质有:每一层的点所属连通块大小和为 \(O(n)\),共有最多 \(O(\log n)\) 层。所以有些动态问题的操作可以直接在点分树上暴力跳祖先修改。

例题 [ZJOI2007]捉迷藏

考虑在点分树上维护两个堆:\(d_u\) 维护 \(u\) 为根的连通块上的所有黑点到 \(fa_u\) (点分树上父亲)的距离,\(ch_u\) 维护 \(u\) 每个子树中到 \(u\) 最远的点,那么答案就是所有 \(ch_u\) 的最大值和次大值和的最大值,每个点的答案也一起放到答案堆中。

每次修改的时候,在点分树上暴力跳,更新 \(d\)\(ch\) 堆,再重新把答案加到答案堆里边去。查询直接输出。

点击查看代码
const int N=1e5+13;
struct Edge{int v,nxt;}e[N<<1];
struct Heap{
	std::priority_queue<int> p,q;
	inline void insert(int x){p.push(x);}
	inline void erase(int x){q.push(x);}
	inline int top(){
		while(!q.empty()&&p.top()==q.top()) p.pop(),q.pop();
		return p.top();
	}
	inline void pop(){
		while(!q.empty()&&p.top()==q.top()) p.pop(),q.pop();
		p.pop();
	}
	inline int setop(){
		int tmp=top(),res;pop();
		res=top();insert(tmp);
		return res;
	}
	inline int size(){return p.size()-q.size();}
}d[N],ch[N],ans;
int n,h[N],etot;
int siz[N],maxx[N],rt,psum,fa[N],dep[N],dis[N][21];
bool vis[N],col[N];
inline void add_edge(int u,int v){e[++etot]=(Edge){v,h[u]};h[u]=etot;}

namespace Tree{
int fa[N],dep[N],siz[N],son[N],top[N];
void dfs1(int u,int f,int deep){
	dep[u]=deep,siz[u]=1,fa[u]=f;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(v==f) continue;
		dfs1(v,u,deep+1);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u,int topf){
	top[u]=topf;
	if(!son[u]) return;
	dfs2(son[u],topf);
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa[u]&&v!=son[u]) dfs2(v,v);
	}
}
inline void init(){dfs1(1,0,0);dfs2(1,1);}
inline int lca(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		u=fa[top[u]];
	}
	return dep[u]<dep[v]?u:v;
}
inline int dist(int u,int v){return dep[u]+dep[v]-2*dep[lca(u,v)];}
}

void findrt(int u,int f){
	siz[u]=1,maxx[u]=0;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(v==f||vis[v]) continue;
		findrt(v,u);
		siz[u]+=siz[v];
		maxx[u]=max(maxx[u],siz[v]);
	}
	maxx[u]=max(maxx[u],psum-siz[u]);
	if(maxx[u]<maxx[rt]) rt=u;
}
void dfs(int u,int f,int dis,Heap &t){
	t.insert(dis);
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;
		if(v!=f&&!vis[v]) dfs(v,u,dis+1,t);
	}
}
void build(int u){
	vis[u]=1;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].v;if(vis[v]) continue;
		rt=0,psum=siz[v];
		findrt(v,0),findrt(rt,0);
		fa[rt]=u,dep[rt]=dep[u]+1;
		dfs(v,0,1,d[rt]);
		ch[u].insert(d[rt].top());
		build(rt);
	}
	ch[u].insert(0);
	if(ch[u].size()>=2) ans.insert(ch[u].top()+ch[u].setop());
	else if(ch[u].size()) ans.insert(ch[u].top());
}
int main(){
//	freopen("9.in","r",stdin);
//	freopen("P2056.out","w",stdout);
	read(n);
	for(int i=1;i<n;++i){
		int u,v;read(u),read(v);
		add_edge(u,v),add_edge(v,u);
	}
	rt=0,maxx[0]=INF,psum=n;
	findrt(1,0),findrt(rt,0);build(rt);
	Tree::init();
	for(int i=1;i<=n;++i)
		for(int j=fa[i];j;j=fa[j]) dis[i][dep[i]-dep[j]]=Tree::dist(i,j);
	int q;read(q);int cnt=0;
	while(q--){
		char op;
		read(op);
		if(op=='C'){
			int u;read(u);
			if(!col[u]){
				if(ch[u].size()>=2) ans.erase(ch[u].top()+ch[u].setop());
				ch[u].erase(0);
				if(ch[u].size()>=2) ans.insert(ch[u].top()+ch[u].setop());
				for(int p=u;fa[p];p=fa[p]){
					if(ch[fa[p]].size()>=2) ans.erase(ch[fa[p]].top()+ch[fa[p]].setop());
					ch[fa[p]].erase(d[p].top());
					d[p].erase(dis[u][dep[u]-dep[fa[p]]]);
					if(d[p].size()) ch[fa[p]].insert(d[p].top());
					if(ch[fa[p]].size()>=2) ans.insert(ch[fa[p]].top()+ch[fa[p]].setop());
				}
			}
			else{
				if(ch[u].size()>=2) ans.erase(ch[u].top()+ch[u].setop());
				ch[u].insert(0);
				if(ch[u].size()>=2) ans.insert(ch[u].top()+ch[u].setop());
				for(int p=u;fa[p];p=fa[p]){
					if(ch[fa[p]].size()>=2) ans.erase(ch[fa[p]].top()+ch[fa[p]].setop());
					if(d[p].size()) ch[fa[p]].erase(d[p].top());
					d[p].insert(dis[u][dep[u]-dep[fa[p]]]);
					ch[fa[p]].insert(d[p].top());
					if(ch[fa[p]].size()>=2) ans.insert(ch[fa[p]].top()+ch[fa[p]].setop());
				}
			}
			col[u]^=1;
		}
		else{
			if(ans.size()) println(ans.top());
			else println(-1);
		}
//		print('\n');print("debug: "),print(++cnt),print(':'),print('\n');
		/*std::queue<int> q;while(!q.empty()) q.pop();
		while(ans.size()) q.push(ans.top()),ans.pop();
		while(!q.empty()) print(q.front()),print(' '),ans.insert(q.front()),q.pop();*/
//		for(int i=1;i<=n;++i) print(i),print(':'),println(ch[i].size());
//		print('\n');
	}
	return 0;
}

回滚莫队

如果莫队只能支持加入/删除其中的一种,那么可以使用回滚莫队。

以只能加入为例,我们把序列每 \(B\) 个分成一块,然后把询问按照左端点所在块升序为第一关键字、右端点升序为第二关键字排序。

依次考虑每个区间,如果当前区间左端点和上一个左端点不在同一个块了,就把此时的 \(l\) 放到当前块右端点 \(+1\)\(r\) 放到当前块右端点并清空答案。这个操作会进行 \(O(\frac{n}{B})\) 次。

如果这个区间的右端点和左端点在同一个块内我们可以直接 \(O(B)\) 暴力计算答案;否则,因为左端点在同一个块的时候右端点是递增的,所以这部分不需要删除并且 \(r\) 总共会移动 \(O(n)\) 次。对于 \(l\) 来说,我们每次都从这个块右端点开始向左扫到区间左端点,这个也不需要删除并且每个询问左端点最多只需要移动 \(O(B)\) 次。

所以总复杂度为 \(O(mB+\frac{n^2}{B})\),取 \(B=\frac{n}{\sqrt m}\) 可得最优复杂度 \(O(n\sqrt m)\)

例题 [JOISC2014] 历史研究

模板 回滚莫队

莫队二次离线

当莫队不能 \(O(1)\) 加入和删除,并且可以把所有的贡献拆成 \(f(x,[l,r])\)(元素对一个区间的贡献),并且贡献有可减性,可以使用莫队二次离线。若每次移动端点的复杂度为 \(O(k)\),那么使用莫队二次离线可以将 \(O(nk\sqrt m)\) 优化为 \(O(nk+n\sqrt m)\)

考虑当前区间为 \([l,r]\),现在要增加右端点至 \(r'\)(其他同理)时对答案的贡献,每个元素 \(x\in [r+1,r']\) 的贡献是 \(f(x,[l,x-1])\),差分一下变成 \(f(x,[1,x-1])-f(x,[1,l-1])\)。这些贡献可以分为两类:

  1. \(x\)\([1,x-1]\)。这个可以简单预处理。
  2. \(x\)\([1,l-1]\)。注意到 \(x\) 也是一段区间(\([r+1,r']\)),所以可以一起记录。

将第二种贡献离线,在跑完莫队之后跑一个序列上的扫描线,然后对每个询问计算答案即可。

我们来看一道例题:

【模板】莫队二次离线

发现可以拆成上述形式,并且贡献有可减性。上莫队二次离线。考虑先预处理出所有 \(\mathrm{popcount}=k\) 的数 \(x\)(个数最多 \(\binom{14}{7}\),大概 \(3000\) 左右),每进来一个数 \(a_i\) 就把桶里所有 \(a_i\oplus x\) 的位置 \(+1\),然后查询就是 \(O(1)\)

点击查看代码
#include<cstdio>
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#define memset __builtin_memset
#define pb push_back
#define popcount __builtin_popcount
typedef long long ll;
const int N=1e5+13,M=20000+13,Lim=16384,B=300;
int n,m,k,a[N],f[N],from[N],tong[M];
ll ans[N];
std::vector<int> num;
struct Ques{
	int l,r,id;ll sum;
	inline bool operator <(const Ques &a)const{return from[l]!=from[a.l]?from[l]<from[a.l]:(from[l]&1?r<a.r:r>a.r);}
}q[N];
struct Item{int l,r,id,op;};
std::vector<Item> b[N];
int main(){
	scanf("%d%d%d",&n,&m,&k);
	if(k>14){
		while(m--) puts("0");
		return 0;
	}
	for(int i=0;i<Lim;++i)
		if(popcount(i)==k) num.pb(i);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
		f[i]=tong[a[i]];
		for(auto x:num) ++tong[a[i]^x];
	}
	for(int i=1;i<=n;++i) from[i]=i/B+1;
	for(int i=1;i<=m;++i) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
	std::sort(q+1,q+m+1);
	for(int i=1,l=1,r=0;i<=m;++i){
		if(l<q[i].l) b[r].pb((Item){l,q[i].l-1,i,-1});
		while(l<q[i].l) q[i].sum+=f[l],++l;
		if(l>q[i].l) b[r].pb((Item){q[i].l,l-1,i,1});
		while(l>q[i].l) --l,q[i].sum-=f[l];
		if(r<q[i].r&&l>1) b[l-1].pb((Item){r+1,q[i].r,i,-1});
		while(r<q[i].r) ++r,q[i].sum+=f[r];
		if(r>q[i].r&&l>1) b[l-1].pb((Item){q[i].r+1,r,i,1});
		while(r>q[i].r) q[i].sum-=f[r],--r;
	}
	memset(tong,0,sizeof tong);
	for(int i=1;i<=n;++i){
		for(auto x:num) ++tong[a[i]^x];
		for(auto x:b[i])
			for(int j=x.l;j<=x.r;++j) q[x.id].sum+=x.op*(tong[a[j]]-(j<=i&&!k));
	}
	for(int i=1;i<=m;++i) ans[q[i].id]=(q[i].sum+=q[i-1].sum);
	for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
	return 0;
}

关于上题的一些感悟:莫队二次离线事实上是一个不太平衡的结构,因为对插入复杂度的要求不高(只有 \(O(n)\) 次),但是由于查询有 \(O(n\sqrt m)\) 次,所以对查询复杂度要求很高(几乎要 \(O(1)\))。

我们再来看一道例题:

[Ynoi2019 模拟赛] Yuno loves sqrt technology II

区间逆序对。发现这个也是可减信息,然后从左边加入/删除和从右边加入/删除需要维护两种信息(区间比一个数大的数、区间比一个数小的数)。然后这个题就需要用到根号平衡了,需要一个 \(O(\sqrt n)\) 单点修改,\(O(1)\) 单点查询的东西,在值域上分块即可。

点击查看代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#define pb push_back
#define memset __builtin_memset
typedef long long ll;
inline void chkmax(int &a,const int &b){if(b>a)a=b;}
const int N=1e5+13,M=500+13,B=350;
int n,m,lim,from[N],L[M],R[M],tot,f[N],g[N],a[N],aa[N],c1[N],c2[N],tag1[M],tag2[M];
struct Ques{
	int l,r,id;ll sum;
	inline bool operator <(const Ques &a)const{return from[l]!=from[a.l]?from[l]<from[a.l]:(from[l]&1?r<a.r:r>a.r);}
}q[N];
struct Node{int l,r,id,op;};
std::vector<Node> b1[N],b2[N];
ll ans[N];
int main(){
#ifdef winterfrost
	freopen("P5047_1.in","r",stdin);
	freopen("P5047.out","w",stdout);
#endif
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),aa[i]=a[i];
	std::sort(aa+1,aa+n+1);lim=std::unique(aa+1,aa+n+1)-aa-1;
	for(int i=1;i<=n;++i) a[i]=std::lower_bound(aa+1,aa+lim+1,a[i])-aa;
	for(int i=1;i<=lim;++i){
		from[i]=i/B+1;
		if(from[i]!=from[i-1]) R[tot]=i-1,L[++tot]=i;
	}
	R[tot]=n;
	for(int i=1;i<=n;++i){
		f[i]=c1[a[i]]+tag1[from[a[i]]];
		for(int j=a[i]+1;j<=R[from[a[i]]];++j) ++c1[j];
		for(int j=from[a[i]]+1;j<=tot;++j) ++tag1[j];
		g[i]=c2[a[i]]+tag2[from[a[i]]];
		for(int j=a[i]-1;j>=L[from[a[i]]];--j) ++c2[j];
		for(int j=from[a[i]]-1;j>=1;--j) ++tag2[j];
	}
	for(int i=1;i<=m;++i) scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
	std::sort(q+1,q+m+1);
	for(int i=1,l=1,r=0;i<=m;++i){
		if(l>q[i].l) b1[r].pb((Node){q[i].l,l-1,i,1});
		while(l>q[i].l) --l,q[i].sum-=f[l];
		if(r<q[i].r&&l>1) b2[l-1].pb((Node){r+1,q[i].r,i,-1});
		while(r<q[i].r) ++r,q[i].sum+=g[r];
		if(l<q[i].l) b1[r].pb((Node){l,q[i].l-1,i,-1});
		while(l<q[i].l) q[i].sum+=f[l],++l;
		if(r>q[i].r&&l>1) b2[l-1].pb((Node){q[i].r+1,r,i,1});
		while(r>q[i].r) q[i].sum-=g[r],--r;
	}
	memset(c1,0,sizeof c1);memset(c2,0,sizeof c2);memset(tag1,0,sizeof tag1);memset(tag2,0,sizeof tag2);
	for(int i=1;i<=n;++i){
		for(int j=a[i]+1;j<=R[from[a[i]]];++j) ++c1[j];
		for(int j=from[a[i]]+1;j<=tot;++j) ++tag1[j];
		for(auto x:b1[i])
			for(int j=x.l;j<=x.r;++j) q[x.id].sum+=x.op*(c1[a[j]]+tag1[from[a[j]]]);
		for(int j=a[i]-1;j>=L[from[a[i]]];--j) ++c2[j];
		for(int j=from[a[i]]-1;j>=1;--j) ++tag2[j];
		for(auto x:b2[i])
			for(int j=x.l;j<=x.r;++j) q[x.id].sum+=x.op*(c2[a[j]]+tag2[from[a[j]]]);
	}
	for(int i=1;i<=m;++i) ans[q[i].id]=(q[i].sum+=q[i-1].sum);
	for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
	return 0;
}
posted @ 2022-05-03 16:30  cunzai_zsy0531  阅读(35)  评论(0编辑  收藏  举报