suxxsfe

一言(ヒトコト)

带修/在线/树上莫队

带修莫队

往常的莫队都是用一个 \(l,r\) 来标识当前的状态,然后每次将这个 \([l,r]\) 的区间不断扩展、缩小
有了修改,那么可以再增减一维,变成 \([l,r,time]\)\(time\) 可以理解为代表了某个时间的数组的状态,比如做一次修改以后,就让时间加一,因为数组的状态改变了
转移的时候,区间的转移和一般莫队相类似,时间上的转移,就是如果这个时间的修改的点在当前的目标区间(就是当前处理的询问的区间)中,那么就删去数组中的原数,加入修改后的新数,变成新一个状态
然后数组也要改(因为 \(time\) 变了,当前的数组肯定对应当前的 \(time\)),为了下面的操作,新数也要和原数交换(比如这一次是“顺向”的进行这个修改,下一次就应该是时间往后退,倒着进行这个修改)

排序方式与块大小、复杂度
类比普通莫队,带修莫队的排序方式应该为以左端点所在的块为第一关键字,右端点所在的块为第二关键字,时间为第三关键字

关于块大小:这似乎才是莫队难的地方,先设块大小为 \(d\),则一共 \(\frac{n}{d}\) 块,并假设修改查询操作都和数列长度相同,且每次转移 \(O(1)\),那么分别分析 \(l,r,time\) 三个指针,注意下面说的一些情况都是 XX 的块 没变,就是他在块内移动,而不是它本身没有移动

  • \(l\) 指针,在块内移动时(这种情况就是因为几个询问左端点相同了,那么按照右端点或时间排序造成的),每次 \(O(d)\),一个 \(n\) 次询问,那么就是 \(O(nd)\)
    移动到下一个块,每次复杂度 \(O(d)\),一共 \(O(\frac{n}{d})\) 次,则总复杂度 \(O(n)\)
  • \(r\) 指针,当 \(l,r\) 都在同一个块,和之前相同,也是 \(n\) 次每次 \(O(d)\),一共 \(O(nd)\)
    \(l\) 的块没变,\(r\) 的块改变,移动到下一个块,单次 \(O(d)\),一共 \(O((\frac{n}{d})^2)\) 次(每次 \(l\) 的块移动一下,\(r\) 的块就要回到最左从新移动,所以是块数的平方),总复杂度 \(O(\frac{n^2}{d})\)
    \(l,r\) 的块都改变,就是上面说的那种 \(r\) 的块移回最左边重新移动的情况,一共块数次,每次 \(O(n)\),那么总复杂度 \(O(\frac{n^2}{d})\)
  • \(time\) 指针,当 \(l,r\) 的块都不变,\(time\) 因为在这一段使得 \(l,r\) 的块不变的询问区间内是排好序的,所以对于这一段询问区间是 \(O(n)\),那么一共有块数的平方个这样的区间,也就是 \(O(\frac{n^2}{d^2})\),那么总复杂度 \(O(\frac{n^3}{d^2})\)
    \(l\) 的块不变,\(r\) 的块改变,以及 \(l,r\) 的块都改变,这两种情况都是单次 \(O(n)\),一共 \(O((\frac{n}{d})^2)\)\(O(\frac{n}{d})\) 次,复杂度则分别为 \(O(\frac{n^3}{d^2})\)\(O(\frac{n^2}{d})\)

综合上面的分析,那么整个算法的复杂度,就是 \(O(\max(nd,\frac{n^2}{d},\frac{n^3}{d^2}))\),由 \(d\le n\) 推出 \(\frac{n^2}{d}\le \frac{n^3}{d^2}\),则总复杂度化简为 \(O(\max(nd,\frac{n^3}{d^2})\),分类讨论一下这两个谁大,就能很简单的得出 \(d=n^{\frac{2}{3}}\) 时最优,是 \(O(n^{\frac{5}{3}})\)

板子,P1903 [国家集训队]数颜色 / 维护队列:https://www.luogu.com.cn/problem/P1903

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
	register int x=0;register int y=1;
	register char c=std::getchar();
	while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
	return y?x:-x;
}
#define N 133340
struct data{
	int l,r,id,time;
}q[N];
int qtot;
struct CHANGE{
	int pos,x;
}change[N];
int n,m,B;
int block[N],a[N];
int cnt[1000005],ans[N];
inline int cmp(data a,data b){
	if(block[a.l]==block[b.l]) return block[a.r]==block[b.r]?a.time<b.time:block[a.r]<block[b.r];
	else return block[a.l]<block[b.l];
}
int num;
inline void add(int x){if(++cnt[x]==1) num++;}
inline void del(int x){if(!--cnt[x]) num--;}
inline void work(int time,int i){
	if(change[time].pos>=q[i].l&&change[time].pos<=q[i].r){
		add(change[time].x);del(a[change[time].pos]);
	}
	std::swap(change[time].x,a[change[time].pos]);
	//这一次被改掉的颜色,就是下一次调用这个函数(参数相同)需要修改成的颜色
}
int main(){
	n=read();m=read();B=std::pow(n,2.0/3);
	for(reg int i=1;i<=n;i++) a[i]=read(),block[i]=block[i-1]+(!((i-1)%B));
	reg int timenow=1;reg char op;
	for(reg int i=1;i<=m;i++){
		op=getchar();
		while(op!='Q'&&op!='R') op=getchar();
		if(op=='Q') q[++qtot].l=read(),q[qtot].r=read(),q[qtot].id=qtot,q[qtot].time=timenow;
		else{
			change[++timenow].pos=read();change[timenow].x=read();
		}
	}
	std::sort(q+1,q+1+qtot,cmp);
	reg int l=1,r=0,time=1;
	for(reg int nexl,nexr,nexT,i=1;i<=qtot;i++){
		nexl=q[i].l;nexr=q[i].r;nexT=q[i].time;
		while(r<nexr) add(a[++r]);
		while(l>nexl) add(a[--l]);
		while(r>nexr) del(a[r--]);
		while(l<nexl) del(a[l++]);
		while(time<nexT) work(++time,i);//先处理完区间,再处理时间
		while(time>nexT) work(time--,i);
		ans[q[i].id]=num;
	}
	for(reg int i=1;i<=qtot;i++) printf("%d\n",ans[i]);
}

树上莫队

就是每次询问是一个路径上的某某信息
一般的莫队是按照询问区间的左右端点相关信息来排序,那么树上的莫队,就尝试把树构造成一个序列,把路径变成一个区间
那么可以用到 dfs 序,不过如果直接把 dfs 序写出来,然后对于一个路径找他们端点之间的区间,发现除了包含路径上的点,还包含了一些子树
如何消去这些子树产生的影响?一般的 dfs 序都是一个点进入 dfs 栈的时候,就把他写进 dfs 序中,现在,在一个点出栈的时候,把他往 dfs 序里再写一遍

然后处理这个 dfs 序序列的时候,以往是每往区间加入一个数字,就加上他的贡献,从区间剔除一个数字,就减他的贡献。而现在,同等的对待往区间加入或从区间删除,如果上一次是加贡献,这次就减贡献,反之亦然(具体实现用一个 vis 记录,每次异或)
为什么要这样?如果第二次遇见这个数,遇见的是序列上同一个数(dfs 序序列上每个数都有两个),那么这次减贡献是显然的,如果不是同一个,说明这次遇见的这个数,是标志着这个数出 dfs 栈,那么应该减去贡献来避免刚才说的那些子树对路径上答案的影响
这样,对于那些被包含进去的本不应该存在的子树,消除了他们的贡献

其他的和序列上的莫队一样了

树上带修莫队

就是把在特殊处理的 dfs 序上,按照上面的方式跑莫队改成了跑带修莫队呗
例题:P4074 [WC2013]糖果公园
https://www.luogu.com.cn/problem/P4074
https://darkbzoj.tk/problem/3052

这题如何加上、减去一个数的贡献都是很显然的,就几乎是个树上带修莫队的板子了

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<map>
#include<iomanip>
#include<cstring>
#define reg register
#define EN std::puts("")
#define LL long long
inline int read(){
	register int x=0;register int y=1;
	register char c=std::getchar();
	while(c<'0'||c>'9'){if(c=='-') y=0;c=std::getchar();}
	while(c>='0'&&c<='9'){x=x*10+(c^48);c=std::getchar();}
	return y?x:-x;
}
#define N 200005
#define M 200005
struct graph{
	int fir[N],nex[M],to[M],tot;
	inline void add(int u,int v){
		to[++tot]=v;
		nex[tot]=fir[u];fir[u]=tot;
	}
}G;
int n,m,B;
struct data{
	int l,r,time,id;
}q[N];
int qtot;
struct Change{
	int pos,val;
}change[N];
int id[N],dfnin[N],dfnout[N],dfscnt;
int deep[N],fa[20][N];
int block[N],col[N],W[N],val[N],vis[N],num[N];
long long Ans[N];
void dfs(int u){
	id[++dfscnt]=u;dfnin[u]=dfscnt;
	deep[u]=deep[fa[0][u]]+1;
	for(reg int i=G.fir[u],v;i;i=G.nex[i]){
		v=G.to[i];
		if(v==fa[0][u]) continue;
		fa[0][v]=u;dfs(v);
	}
	id[++dfscnt]=u;dfnout[u]=dfscnt;
}
inline int lca(reg int u,reg int v){
	if(deep[u]<deep[v]) u^=v,v^=u,u^=v;
	for(reg int i=18;~i;i--)if(deep[fa[i][u]]>=deep[v]) u=fa[i][u];
	if(u==v) return u;
	for(reg int i=18;~i;i--)if(fa[i][u]^fa[i][v]) u=fa[i][u],v=fa[i][v];
	return fa[0][u];
}
inline int cmp(data a,data b){
	if(block[a.l]==block[b.l]) return block[a.r]==block[b.r]?a.time<b.time:block[a.r]<block[b.r];
	return block[a.l]<block[b.l];
}
inline void init_q(int Q){
	int timenow=1;
	for(reg int i=1,x,y,op;i<=Q;i++){
		op=read();x=read();y=read();
		if(op){
			if(dfnin[x]>dfnin[y]) x^=y,y^=x,x^=y;
			q[++qtot]=(data){lca(x,y)==x?dfnin[x]:dfnout[x],dfnin[y],timenow,qtot};
		}
		else{
			change[++timenow]=(Change){x,y};
		}
	}
	std::sort(q+1,q+1+qtot,cmp);
}
LL ans;
inline void add(int x){
	if(vis[x]) ans-=(long long)val[col[x]]*W[num[col[x]]--];
	else ans+=(long long)val[col[x]]*W[++num[col[x]]];
	vis[x]^=1;
}
inline void chtime(Change &x){//传引用!!!
	int lastcol=col[x.pos];
	if(vis[x.pos]){//计算过这个点,需要先把他的贡献减去,修改完再加回来
		add(x.pos);
		col[x.pos]=x.val;
		add(x.pos);
	}
	else col[x.pos]=x.val;
	x.val=lastcol;
}
int main(){
	n=read();m=read();int Q=read();
	B=std::pow(2*n,2.0/3);
	for(reg int i=1;i<=2*n;i++) block[i]=block[i-1]+(!((i-1)%B));
	for(reg int i=1;i<=m;i++) val[i]=read();
	for(reg int i=1;i<=n;i++) W[i]=read();
	for(reg int i=1,u,v;i<n;i++){
		u=read();v=read();
		G.add(u,v);G.add(v,u);
	}
	for(reg int i=1;i<=n;i++) col[i]=read();
	dfs(1);
	for(reg int i=1;i<=18;i++)
		for(reg int j=1;j<=n;j++) fa[i][j]=fa[i-1][fa[i-1][j]];
	init_q(Q);
	reg int l=1,r=0,time=1;
	for(reg int i=1,nexl,nexr,nexT;i<=qtot;i++){
//			printf("now : %d %d %d\n",l,r,time);
		nexl=q[i].l;nexr=q[i].r;nexT=q[i].time;
		while(r<nexr) add(id[++r]);
		while(l>nexl) add(id[--l]);
		while(r>nexr) add(id[r--]);
		while(l<nexl) add(id[l++]);
		while(time<nexT) chtime(change[++time]);
		while(time>nexT) chtime(change[time--]);
		int Lca=lca(id[l],id[r]);
		if((Lca^id[l])&&(Lca^id[r])){//lca 需要另外计算
			add(Lca);
			Ans[q[i].id]=ans;
			add(Lca);
		}
		else Ans[q[i].id]=ans;
//			printf("id : %d  ans : %lld\n",q[i].id,ans);
	}
	for(reg int i=1;i<=qtot;i++) printf("%lld\n",Ans[i]);
	return 0;
}

在线莫队

在线莫队的操作是从这里看的,orz 诗乃:https://www.luogu.com.cn/blog/asadashino/moqueue

一般的莫队是把询问排序的,打乱无法保证在原顺序中,回答下一个询问前一定知道上一个询问的答案,所以强制在线的情况下想排序询问肯定不可能,但是莫队那种转移区间的方式还是可以用的
方法就是选取一些“特征点”,然后预处理任意两个“特征点”之间的答案和转移这些区间要用到的信息,称之为特征区间,设最小特征区间的长度为 \(d\),那么预处理的复杂度就是 \(O(n\cdot \frac{n}{d})=O(\frac{n^2}{d})\)(就是固定住一个点,另一个点不断向外扩展到下一个特征点,然后扩展到最右就移动固定的点,固定的点移动 \(\frac{n}{d}\) 次),然后询问就通过移动区间到任意一个特征区间,单次复杂度 \(O(d)\),显然 \(d=\sqrt n\) 时最优,那么总复杂度依然 \(O(n\sqrt n)\)
所以其实还是比较简单的

但是之前说过要维护特征区间的转移信息,肯定不能每个区间都单独维护,如果这个信息满足可减性,那么直接用前缀和的思想减一下就行,否则可能得用可持久化等东西?反正我不会就是了

在线带修莫队

去看诗乃的博客吧,看了看好复杂的样子

posted @ 2020-08-20 22:35  suxxsfe  阅读(263)  评论(5编辑  收藏  举报