带修莫队与树上莫队
带修莫队与树上莫队
带修莫队
我们把每一个修改编号,并把这个编号称为时间戳,查询操作的时间戳就沿用最近一次修改操作的时间戳。
对于每个查询操作,如果当前时间戳相对太大了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。
只有当当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。
简单的说,就是搞一个指针用于跳修改操作,若是跳多了就往前跳,跳少了就往后跳
这样我们的指针移动方向就由四个变成了六个:
对于块的大小可以取 \(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]);
}