树上莫队、带修莫队

树上莫队

引入

树上莫队,美其名曰在树上进行莫队操作,即区间维护、查询

但之前的莫队都在线性区间操作,对于树上莫队,考虑转化树形结构为链式结构

使用——欧拉序

解决

欧拉序:

对于一棵树进行 dfs ,每个点遍历时压入栈,记录进序列,回溯到这个点时出栈,再记录进序列一次,即每个点在序列中出现了两次。

在树上查询 u 到 v 的简单路径 (u 深度比 v 小), 跟欧拉序有什么关系? 分类讨论一下 

首先,记录点 x 在欧拉序中分别先后出现在 st[x] 和 ed[x] 

1、LCA(u,v) = u  ,即 v 在 u 的子树中,那么我们区间 u~v 包含的点必然在序列的 st[u]~st[v] 中

2、LCA(u,v) != u ,那么路径上的点不在 u 和 v 的子树里。所以我们要找的点在 ed[u]~st[v] 之间。还要特判掉一个 lca。

tips:显然区间中还会有一些不相干的点,简单思考一下会发现这是其他的一些子树,所以他们必然在序列中出现2次,所以保留次数为1的点即可。

例题

SP10707,板子题,贴代码看注释 ↓

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10,M=1e5+10,P=20;
int n,nn,m,dfn[N],st[N],en[N],fa[N][P];
int a[N],aa[N],cnt[N],timeset;
int vis[N],ans[M],temp,bs,b[N];//vis记录当前每个数出现了几次 
vector<int> G[N];
struct ques{
	int l,r,id,lca;
}q[M];
bool cmp(ques x,ques y){
	if(b[x.l]!=b[y.l]) return x.l<y.l;
	if(b[x.l] & 1) return x.r < y.r ;
	return x.r > y.r;
}
void dfs(int u,int faf) {
	st[u]=++timeset;//起点 
	dfn[timeset] = u;//欧拉序 
	fa[u][0] = faf;
	for(int i=1;i<P;i++) {
		fa[u][i] = fa[fa[u][i-1]][i-1];//顺便LCA预处理 
	}
	for(auto v : G[u] ) {
		if( v != faf) dfs(v , u );
 	}
 	en[u] = ++timeset;//终点 
 	dfn[timeset] = u;//欧拉序 
}
int lca (int x,int y) {
	if(x==y) return x;
	if( st[y] < st[x] ) swap(x,y);
	for(int i=P-1;i>=0;i--) {
		if( st[ fa[y][i] ] > st[x]) {
			y = fa[y][i];
		}
	}
	if(x==y) return x;
	for(int i=19;i>=0;i--) if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
	return fa[x][0];
} 
void add(int x) {
	vis[x]++;
	if(vis[x]==1) {
		cnt[a[x]] ++;
		if(cnt[a[x]]==1) temp++;
	}
	if(vis[x]==2) { 
		cnt[a[x]]--;
		if(cnt[a[x]]==0) temp--;
	}
}
void del(int x) {
	vis[x]--;
	if(vis[x] == 1) { //出现过两次的删除时做正贡献 
		cnt[a[x]] ++;
		if(cnt[a[x]]==1) temp++;
	}
	if(vis[x] == 0) {
		cnt[a[x]]--;
		if(cnt[a[x]]==0 ) temp--;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	bs=sqrt(2*n);//每个点有两个 块长不一定,但大约在根号附近
	for(int i=1;i<=n;i++){
		scanf("%d" ,&a[i]);
		aa[i]=a[i];
	}
	sort(aa+1,aa+n+1);
	nn=unique(aa+1,aa+n+1) - aa -1;
	for(int i=1;i<=n;i++) {
		a[i]=lower_bound(aa+1,aa+nn+1,a[i])-aa;
	}//离散化 
	for(int i=1;i<n;i++) {
		int u,v;
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	for(int i=1;i<=2*n;i++) b[i]=(i-1)/bs+1;
	dfs(1,0);//求欧拉序 
	for(int i=1;i<=m;i++) {
		int u,v;
		scanf("%d%d",&u,&v);
		if(st[v]<st[u]) swap(u,v);//入栈早的是左端点
		int lc=lca(u,v);
		if(lc==u) q[i]=(ques){st[u],st[v],i,0};
		else q[i]=(ques){en[u],st[v],i,lc};//处理询问,需要记录LCA!!! 
	}
	sort(q+1,q+1+m,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++) {
		while(r<q[i].r) add(dfn[++r]);
		while(l>q[i].l) add(dfn[--l]);
		while(r>q[i].r) del(dfn[r--]);
		while(l<q[i].l) del(dfn[l++]);
		if(q[i].lca!=0) add(q[i].lca);
		ans[q[i].id]=temp;
		if(q[i].lca!=0) del(q[i].lca);
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	 
	return 0;
} 

带修莫队

引入

因为莫队是离线操作,所以看似无法进行修改维护,但带修莫队利用多一维时间维,处理每一次移动时,看这次移动前进行了哪些修改,如果修改多了,就重置一些,反之修改一些。

解决

对于题目 LUOGU 数颜色 

假设我把每个修改的询问记录为 $modify[++cnt] = (pos , x)$(表示把位置pos 修改为 x )

普通莫队记录询问 q[i] = ( l , r , id),这里就记录多一个变为 q[i] =(l,r,id,cnt)

为什么要记录每次查询时进行了几个修改呢,我们除了双指针 L ,R 外在来一个 T ,每次区间查询双指针移动完之后,对于 q[i].t<T 的 删掉这个修改操作,T-- ;反之加上

怎么删掉一个修改操作?一个修改如果被删掉多次是什么?

我们会发现,不管对于删掉每个修改操作还是加上,本质上就是把修改后的数与原数组交换,所以每次修改完之后,把原数组位置 pos 的数赋值给这个修改操作,即 modi[T].x=a[pos],下一次不管是删除这个操作还是加上这个操作,都等于a[pos]=modi[T].x . 这样就实现了删掉 or 加上一个修改操作

代码加注释贴贴

注意 modify 函数的细节

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	char c=getchar();int x=0,f=1;
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=x*10+c-'0',c=getchar();}
	return x*f;
}//莫队题是这样的(老是要卡常)
const int N=2e5+10;
int n,m,a[N],cnt[5*N],temp,b[N],bs,ans[N];
struct ques{
	int l,r,id,t;//左端点、右端点、第几个询问、在第几次修改操作之后
}q[N];
struct mody{
	int x,p;
}modi[N];
void add(int x){
	cnt[x]++;
	if(cnt[x]==1) temp++;
}
void del(int x) {
	cnt[x]--;
	if(cnt[x]==0) temp--;
}//与普通莫队一样
int n1,n2;
void modify(int x,int i){
	if(modi[x].p>=q[i].l&&modi[x].p<=q[i].r){//!!!要判断这个修改在不在区间里面,否则不能改
		add(modi[x].x);
		del(a[modi[x].p]);
	}
	swap(a[modi[x].p],modi[x].x);
    //但不管在不在区间里都得交换,不然影响后面的移动(后面可能会包含这个地方)
}
bool cmp(ques x,ques y){
	if(b[x.l]==b[y.l]&&b[x.r]==b[y.r]) return x.t<y.t;
	if(b[x.l]==b[y.l]) return b[x.r]<b[y.r];
	return b[x.l]<b[y.l];//不能奇偶优化,且增加第三维时间维优先
}
int main(){
	n=read();
	m=read();
	bs=pow(n,0.666);//证明复杂度大约为 n^(3/5),但建议块长还是(2/3)
	for(int i=1;i<=n;i++) {
		a[i]=read();
		b[i]=(i-1)/bs+1;
	}
	for(int i=1;i<=m;i++) {
		char tet,c[3];
		int l,r;
		scanf("%s",c);
		l=read();
		r=read();
		tet=c[0];
		if(tet=='Q') {
			q[++n1].l=l;
			q[n1].r=r;
			q[n1].id=n1;
			q[n1].t=n2;//记录时间维
		}
		else{
			modi[++n2].p=l;
			modi[n2].x=r;//记录修改询问
		}
	}
	sort(q+1,q+n1+1,cmp);
	int r=0,l=1,tt=0;
	for(int i=1;i<=n1;i++) {
		while(r<q[i].r) add(a[++r]);
		while(r>q[i].r) del(a[r--]);
		while(q[i].l<l) add(a[--l]);
		while(q[i].l>l) del(a[l++]);
		while(tt<q[i].t) modify(++tt,i);
		while(tt>q[i].t) modify(tt--,i);
		ans[q[i].id] = temp;
	}
	for(int i=1;i<=n1;i++) {
		printf("%d\n",ans[i]);
	}
	
	return 0;
}

完结撒花——<-^^->——

下期预告,带修树上莫队混合题分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值