Loading

带修莫队与树上莫队

带修莫队与树上莫队

\(\to\text{参考}\leftarrow\)

带修莫队

我们把每一个修改编号,并把这个编号称为时间戳查询操作的时间戳就沿用最近一次修改操作的时间戳

对于每个查询操作,如果当前时间戳相对太大了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。

只有当当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。

简单的说,就是搞一个指针用于跳修改操作,若是跳多了就往前跳,跳少了就往后跳

这样我们的指针移动方向就由四个变成了六个:

\[[l-1,r,t],[l+1,r,t],[l,r-1,t],[l,r+1,t],[l,r,t+1],[l,r,t-1] \]

对于块的大小可以取 \(n^{\frac 2 3}\) 可以达到一个较优解,时间复杂度 \(O(n^{\frac 5 3})\)

代码部分,主要改动的有两个:排序和时间戳的修改

对于时间戳修改操作:

inline void upd(int x,int t){
	if(qa[x].l<=qb[t].l&&qb[t].l<=qa[x].r){
		del(a[qb[t].l]);
		add(qb[t].r);
	}
	swap(a[qb[t].l],qb[t].r);//把值交换一次相当于修改,交换两次就等于没有修改
}

对于排序操作,多了一维 \(t\) ,因此排序就要按 \(l,r\) 所在的块内 \(t\) 递增来排 (原来的莫队是按 \(l\) 所在的块内 \(r\) 递增来排):

inline bool cmp(Q a,Q b){
	return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.t<b.t);
}

例题:P1903 [国家集训队] 数颜色 / 维护队列

思路

如上

code

#include<bits/stdc++.h>
using namespace std;

const int N=133335;
const int M=1000005;

inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}

int n,m,sz,sum,cnta,cntb;
int cnt[M],a[N],ans[N],bl[N];

struct Q{
	int l,r,t,id;
}qa[N],qb[N];

inline bool cmp(Q a,Q b){
	return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.t<b.t);
}

inline void add(int x){sum+=!cnt[x]++;}
inline void del(int x){sum-=!--cnt[x];}

inline void upd(int x,int t){
	if(qa[x].l<=qb[t].l&&qb[t].l<=qa[x].r){
		del(a[qb[t].l]);
		add(qb[t].r);
	}
	swap(a[qb[t].l],qb[t].r);//把值交换一次相当于修改,交换两次就等于没有修改
}

signed main(){
	n=read(),m=read();
	sz=pow(n,2.0/3.0);
	for(int i=1;i<=n;++i){
		a[i]=read();
		bl[i]=(i-1)/sz+1;
	}
	for(int i=1;i<=m;++i){
		char ch;
		cin>>ch;
		int l=read(),r=read();
		if(ch=='Q') qa[++cnta]={l,r,cntb,cnta};
		else qb[++cntb]={l,r,0,0};
	}
	sort(qa+1,qa+cnta+1,cmp);		 //莫
	int l=1,r=0,t=0;				 //队
	for(int i=1;i<=cnta;++i){
		while(l>qa[i].l) add(a[--l]);//基
		while(l<qa[i].l) del(a[l++]);//本
		while(r>qa[i].r) del(a[r--]);//操
		while(r<qa[i].r) add(a[++r]);//作
		while(t<qa[i].t) upd(i,++t); //带
		while(t>qa[i].t) upd(i,t--); //修
		ans[qa[i].id]=sum;
	}
	for(int i=1;i<=cnta;++i) printf("%d\n",ans[i]);
}

树上莫队

树上莫队的核心思想:通过欧拉序把树上的链的操作转换成区间上的操作,因为 \(dfs\) 序是无法做到这一点的。

欧拉序\(dfs\) 一遍整个树,每遍历到一个节点或是要结束遍历的的时候将这个节点加入序列,最终得到的序列就是这棵树的欧拉序。

显然每个数都出现了两次

例如这样一棵树:

那么如何把树上问题转换为区间问题呢?

简单来说,找到两个数 \(a,b\) 之间的所有数,若出现 \(1\) 次则说明该数在链 \(a,b\) 上,出现两次就说明不在

对于莫队来说就是搞一个 \(vis\) 数组记录,若一个点没记录过就加(\(1\) 次),记录过就减(\(2\) 次)

具体代码长这样:

inline void work(int x){
	vis[x]?del(x):add(x);//欧拉序中出现两次的不在路径上,出现一次的在路径上
	vis[x]^=1;
}

但是有一个特殊情况\(a,b\)\(lca\) 也在链上,但若 \(lca\neq a\)\(lca\neq b\)\(lca\) 就不会在 \([a,b]\)

这个就需要分类讨论,我们 \(pos[i].f\) 表示其第一次出现的位置,\(pos[i].s\) 表示其第二次出现的位置

假定 \(pos[a].f < pos[b].f\) (若不然则交换)

  • \(lca(a,b)= a\),则我们用 \([pos[a].f,pos[b].f]\) 这个区间
  • 否则我们用 \([pos[a].s,pos[b].f]\) 这个区间 (因为 \([pos[a].f,pos[a].f)\) 这个区间不会再路径上)

然后对于第二种情况我们另外修改 \(lca(a,b)\) 即可

例题:P4074 [WC2013] 糖果公园

思路

这题是一个树上带修莫队

用欧拉序把它转换为一个带修莫队问题即可

#include<bits/stdc++.h>
using namespace std;

const int N=2e6+5;

#define ll long long
#define int long long

inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}

struct pii{
	int f,s;
}pos[N];

struct Q{
	int l,r,lca,t,id;
}q[N];

struct U{
	int x,v;
}c[N];

int n,m,qaq,tot,sz,qcnt,ccnt;
ll sum,ans[N];
int v[N],w[N],cnt[N],bl[N],val[N];
int dep[N],f[N],dfn[N*2],top[N],son[N],size[N];
bool vis[N];
vector <int> G[N];

inline bool cmp(Q a,Q b){
	return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.t<b.t);
}

inline void dfs1(int x,int fa){
	dep[x]=dep[fa]+1,f[x]=fa,size[x]=1;
	dfn[++tot]=x;
	pos[x].f=tot;
	int maxson=-1;
	for(auto y:G[x]){
		if(y==fa) continue;
		dfs1(y,x);
		size[x]+=size[y];
		if(maxson<size[y]) maxson=size[y],son[x]=y;
	}
	dfn[++tot]=x;
	pos[x].s=tot;
}

inline void dfs2(int x,int tp){
	top[x]=tp;
	if(!son[x]) return;
	dfs2(son[x],tp);
	for(auto y:G[x])
		if(y!=f[x]&&y!=son[x]) dfs2(y,y);
}

inline int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=f[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}

inline void add(int x){sum+=1ll*v[val[x]]*w[++cnt[val[x]]];}
inline void del(int x){sum-=1ll*v[val[x]]*w[cnt[val[x]]--];}
inline void work(int x){
	vis[x]?del(x):add(x);//欧拉序中出现两次的不在路径上,出现一次的在路径上
	vis[x]^=1;
}

inline void upd(int t){
	if(vis[c[t].x]){
		work(c[t].x);
		swap(val[c[t].x],c[t].v);
		work(c[t].x);
	}
	else swap(val[c[t].x],c[t].v);
}

signed main(){
	n=read(),m=read(),qaq=read();
	sz=pow(2*n,0.667);
	for(int i=1;i<=m;++i) v[i]=read();
	for(int i=1;i<=n;++i) w[i]=read();
	for(int i=1;i<n;++i){
		int x=read(),y=read();
		G[x].push_back(y);
		G[y].push_back(x);
	}
	dfs1(1,0);						   //树
	dfs2(1,1);						   //上
	for(int i=1;i<=tot;++i) bl[i]=(i-1)/sz+1;
	for(int i=1;i<=n;++i) val[i]=read();
	for(int i=1;i<=qaq;++i){
		int op=read(),a=read(),b=read();
		if(op){
			//l,r,lca,t,id
			int LCA=lca(a,b);
			if(pos[a].f>pos[b].f) swap(a,b);
			if(a==LCA) q[++qcnt]={pos[a].f,pos[b].f,0,ccnt,qcnt};
			else q[++qcnt]={pos[a].s,pos[b].f,LCA,ccnt,qcnt};//欧拉序中若两点lca不为两者之一,则lca不在区间内,需要额外记录
		}
		else c[++ccnt]={a,b};
	}
	sort(q+1,q+qcnt+1,cmp);			   //莫
	int l=1,r=0,t=0;				   //队
	for(int i=1;i<=qcnt;++i){
		while(l<q[i].l) work(dfn[l++]);//基
		while(l>q[i].l) work(dfn[--l]);//本
		while(r>q[i].r) work(dfn[r--]);//操
		while(r<q[i].r) work(dfn[++r]);//作
		while(t<q[i].t) upd(++t);	   //带
		while(t>q[i].t) upd(t--);	   //修
		if(q[i].lca) work(q[i].lca);   //特
		ans[q[i].id]=sum;
		if(q[i].lca) work(q[i].lca);   //判
	}
	for(int i=1;i<=qcnt;++i) printf("%lld\n",ans[i]);
}
posted @ 2022-07-15 12:05  Into_qwq  阅读(13)  评论(0编辑  收藏  举报