树上莫队
引入
树上莫队,美其名曰在树上进行莫队操作,即区间维护、查询
但之前的莫队都在线性区间操作,对于树上莫队,考虑转化树形结构为链式结构
使用——欧拉序
解决
欧拉序:
对于一棵树进行 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 数颜色
假设我把每个修改的询问记录为 (表示把位置pos 修改为 x )
普通莫队记录询问 ,这里就记录多一个变为
为什么要记录每次查询时进行了几个修改呢,我们除了双指针 L ,R 外在来一个 T ,每次区间查询双指针移动完之后,对于 的 删掉这个修改操作,T-- ;反之加上
怎么删掉一个修改操作?一个修改如果被删掉多次是什么?
我们会发现,不管对于删掉每个修改操作还是加上,本质上就是把修改后的数与原数组交换,所以每次修改完之后,把原数组位置 pos 的数赋值给这个修改操作,即 ,下一次不管是删除这个操作还是加上这个操作,都等于 . 这样就实现了删掉 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;
}
完结撒花——<-^^->——
下期预告,带修树上莫队混合题分析