带修莫队与树上莫队
带修莫队与树上莫队#
带修莫队#
我们把每一个修改编号,并把这个编号称为时间戳,查询操作的时间戳就沿用最近一次修改操作的时间戳。
对于每个查询操作,如果当前时间戳相对太大了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。
只有当当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。
简单的说,就是搞一个指针用于跳修改操作,若是跳多了就往前跳,跳少了就往后跳
这样我们的指针移动方向就由四个变成了六个:
对于块的大小可以取 可以达到一个较优解,时间复杂度
代码部分,主要改动的有两个:排序和时间戳的修改
对于时间戳修改操作:
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);//把值交换一次相当于修改,交换两次就等于没有修改
}
对于排序操作,多了一维 ,因此排序就要按 所在的块内 递增来排 (原来的莫队是按 所在的块内 递增来排):
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]);
}
树上莫队#
树上莫队的核心思想:通过欧拉序把树上的链的操作转换成区间上的操作,因为 序是无法做到这一点的。
欧拉序: 一遍整个树,每遍历到一个节点或是要结束遍历的的时候将这个节点加入序列,最终得到的序列就是这棵树的欧拉序。
显然每个数都出现了两次
例如这样一棵树:
那么如何把树上问题转换为区间问题呢?
简单来说,找到两个数 之间的所有数,若出现 次则说明该数在链 上,出现两次就说明不在
对于莫队来说就是搞一个 数组记录,若一个点没记录过就加( 次),记录过就减( 次)
具体代码长这样:
inline void work(int x){
vis[x]?del(x):add(x);//欧拉序中出现两次的不在路径上,出现一次的在路径上
vis[x]^=1;
}
但是有一个特殊情况: 的 也在链上,但若 且 , 就不会在 内
这个就需要分类讨论,我们 表示其第一次出现的位置, 表示其第二次出现的位置
假定 (若不然则交换)
- 若 ,则我们用 这个区间
- 否则我们用 这个区间 (因为 这个区间不会再路径上)
然后对于第二种情况我们另外修改 即可
例题: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]);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现