前言
真不知道是哪个傻子调了一个小时代码,试图找到解决区间左边界转移问题的解决方案,冥思苦想却无计可施,最后却发现一定会被重置的lca在某些情况下没有参与统计答案……
前置知识
oiwiki真的好用!(来自luckyxun的感慨)
题目描述
洛谷P4074 [WC2013] 糖果公园,这里就不再赘述。
分析
作为一个刚学完莫队没多久的蒟蒻,看到这会因两个点不同而改变的答案贡献(不易合并),这树上两点的简单路径(易想到欧拉序,也就有了区间),这对点的修改操作,想到带修莫队这不是理所当然嘛!
但是! 第一个难点也来了:光靠欧拉序没法做啊!
一.区间的获取
欧拉序只是在对树进行dfs时,记录了经过的点,光靠它无法进行区间边界的转移。不过,仔细观察欧拉序,你会发现一个事实:通过欧拉序,你可以得到每个点的出入情况。
根据这一点,我们可以很轻易地想到:只要在对树进行dfs时,记录下每个点的进出情况,不就可以进行区间边界的转移了吗!暂且叫这个新的序列为进出序。
相关代码如下:
- pel[i]表示进出序为i的点为x。
- per[i]表示点x第一次被遍历时的进出序。
//在树上跑dfs,得到每个点的经过情况
void dfs(int x,int f){
pel[++tp]=x;// 入
per[x]=tp;
st[tp][0]=x;
for(int i=h[x];i;i=p[i].ne){
int v=p[i].to;
if(v!=f){
dfs(v,x);
pel[++tp]=x;//出
st[tp][0]=x;
pel[++tp]=x;//入
st[tp][0]=x;
}
}
pel[++tp]=x;//出
st[tp][0]=x;
}
二.区间边界的转移
现在我们已经得到了进出序,那就该想想如何进行莫队中区间边界的转移。
根据进出序中,每个点都有进、出两种状态,且奇数次经过为进、偶数次经过为出,可以轻易地联想到异或,也就联想到了区间边界的转移:
- 用ins[i]表示在区间(l,r)中,点 i 的状态,1为进入状态,0为未进状态。
- 在区间边界变化时,根据边界点的状态,进行相应的带修莫队常规转移操作。
相关代码如下:
//删除贡献
void del(int x){
int ty=c[x];
sum-=(long long) v[ty]*w[cnt[ty]--];
ins[x]=0;
}
//加上贡献
void addd(int x){
int ty=c[x];
sum+=(long long) v[ty]*w[++cnt[ty]];
ins[x]=1;
}
l=1,r=0,t=0;
for(int i=1;i<=ta;i++){
//修改操作
while(t<ak[i].t){
t++;
int x=cg[t].x;
if(ins[x]){//需强行修改
del(x);
swap(cg[t].y,c[x]);
addd(x);
}
else{
swap(cg[t].y,c[x]);
}
}
while(t>ak[i].t){
int x=cg[t].x;
if(ins[x]){
del(x);
swap(cg[t].y,c[x]);
addd(x);
}
else{
swap(cg[t].y,c[x]);
}
t--;
}
//不带修改的莫队操作
int x=per[ak[i].x],y=per[ak[i].y];
if(x>y) swap(x,y);
while(r>y) cga(pel[r--]);
while(r<y) cga(pel[++r]);
while(l>x) cga(pel[--l]);
while(l<x) cga(pel[l++]);
但是! 只是这样转移是不对的!如题目所给样例:
所得到的进出序为:1 3 4 4 3 3 2 2 3 3 3 1 1 1
这时候你就会发现,样例中,询问1和3的答案是正确的,询问2和4却是错的!
可是为什么会错呢?
以第二次询问为例:
x | ins[x] |
---|---|
1 | 0 |
2 | 1 |
3 | 0 |
4 | 0 |
根据表格和图示,可以很明显地发现问题:三号点和四号点并没有对答案做出贡献!
手动模拟区间边界的转移过程,就可以发现作为lca的三号点被经过了四次,作为左边界的四号点被经过了两次。
这个时候,多造数据模拟一下,观察一下,就可以大胆地猜测一下:
- 若两点的lca并不是两点中的一个,则lca的贡献一定会被删除。
- 左边界的点被经过的次数少了一。
这也就是树上莫队不同于普通莫队的地方,而要处理这两个问题,只需要先把左边界的点的经过次数加一,这时就会发现lca一定不会贡献答案,接着再特判lca即可。
记得在统计完答案后,lca的贡献一定要删除!!!
完整代码
#include <bits/stdc++.h>
using namespace std;
/*
1.树上的点存在两个状态:入和出
2.需要记录dfs遍历树时,经过的点的顺序(状态不同的相同点当作不同点以同一序号记录)
3.记录每个点第一次进入时的下标,作为边界进行计算和求lca(类似于欧拉序)
4.两点的公共祖先会被二次经过,故需特判加上,答案统计完后再删除
5.按照经过点的顺序的统计数分块后,跑莫队,之前 ins[x]状态为0则加上数据,否则减去
6.在跑莫队时,左边界往右扩展时,需注意是<=而非<,因为左边界的那个点也得参加统计
*/
const int N=1e6+5;
int n,m,q,cnt[N],l,r,pel[N<<3],per[N],tp,c[N],h[N],tot,blocksize,x,y,ty,tq,ta,t,st[N<<3][35],lg[N<<3];
long long v[N],w[N],sum,ans[N];
bool ins[N];
struct P{
int ne,to;
}p[N<<1];
struct Q{
int x,y,t,id;
}ak[N],cg[N];
//加边
void add(int f,int to){
p[++tot].ne=h[f];
p[tot].to=to;
h[f]=tot;
}
//在树上跑dfs,得到每个点的经过情况
void dfs(int x,int f){
pel[++tp]=x;// 入
per[x]=tp;
st[tp][0]=x;
for(int i=h[x];i;i=p[i].ne){
int v=p[i].to;
if(v!=f){
dfs(v,x);
pel[++tp]=x;//出
st[tp][0]=x;
pel[++tp]=x;//入
st[tp][0]=x;
}
}
pel[++tp]=x;//出
st[tp][0]=x;
}
//分块
int get(int x){
return (x-1)/blocksize+1;
}
//询问排序
bool cmp(Q a,Q b){
int xa=get(per[a.x]),xb=get(per[b.x]);
int ya=get(per[a.y]),yb=get(per[b.y]);
return xa==xb?(ya==yb?a.t<b.t:ya<yb):xa<xb;
}
//加上贡献
void addd(int x){
int ty=c[x];
sum+=(long long) v[ty]*w[++cnt[ty]];
ins[x]=1;
}
//删除贡献
void del(int x){
int ty=c[x];
sum-=(long long) v[ty]*w[cnt[ty]--];
ins[x]=0;
}
//更新答案
void cga(int x){
if(ins[x]){//已经计算过了,就删除
del(x);
}
else{//未计算过,则加入
addd(x);
}
}
//用于st表中的比较
int minn(int x,int y){
return per[x]<per[y]?x:y;
}
//求两个点的lca
int fd(int x,int y){
int k=lg[x-y+1];
return minn(st[y][k],st[x-(1<<k)+1][k]);
}
int main(){
cin>>n>>m>>q;
for(int i=1;i<=m;i++){
scanf("%lld",&v[i]);
}
for(int j=1;j<=n;j++){
scanf("%lld",&w[j]);
}
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++){
scanf("%d",&c[i]);
}
dfs(1,0);//对树进行dfs
//预处理出log2的值
lg[1]=0;//
for(int i=2;i<=tp;i++){
lg[i]=lg[i>>1]+1;
}
//求st表
for(int j=1;j<=25;j++){
for(int i=1;i+(1<<j)<=tp;i++){
st[i][j]=minn(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
}
blocksize=pow(tp,2.0/3);//分块大小
for(int i=1;i<=q;i++){
scanf("%d%d%d",&ty,&x,&y);
if(ty){
ta++;
ak[ta].id=ta;
ak[ta].t=tq;
ak[ta].x=x;
ak[ta].y=y;
}
else{
cg[++tq].x=x;
cg[tq].y=y;
}
}
sort(ak+1,ak+1+ta,cmp);
l=1,r=0,t=0;
for(int i=1;i<=ta;i++){
//修改操作
while(t<ak[i].t){
t++;
int x=cg[t].x;
if(ins[x]){//需强行修改
del(x);
swap(cg[t].y,c[x]);
addd(x);
}
else{
swap(cg[t].y,c[x]);
}
}
while(t>ak[i].t){
int x=cg[t].x;
if(ins[x]){
del(x);
swap(cg[t].y,c[x]);
addd(x);
}
else{
swap(cg[t].y,c[x]);
}
t--;
}
//不带修改的莫队操作
int x=per[ak[i].x],y=per[ak[i].y];
if(x>y) swap(x,y);
while(r>y) cga(pel[r--]);
while(r<y) cga(pel[++r]);
while(l>x) cga(pel[--l]);
while(l<=x) cga(pel[l++]);//重点!!!是<=不是<!!!
//求lca并特判
int lca=fd(y,x);
addd(lca);
ans[ak[i].id]=sum;//统计答案
del(lca);
}
for(int i=1;i<=ta;i++){
printf("%lld\n",ans[i]);//输出
}
return 0;
}
题外话
第一次写博客,表达可能不是很好,只能尽力把我做题时的想法和踩的坑写出来了。