BZOJ 3637: Query on a tree VI (树链剖分+树状数组)

BZOJ 3637: Query on a tree VI


题意概述:

给一棵n个结点的树,结点有黑白两色,一开始全为黑色.
对于q个操作,每个操作由两个整数op,u给出.
当op=0,将u点颜色反转.
当op=1,求与u点相连的点的个数(若两点及两点间路径上均为同色点,则两点相连,否则不相连),即求从u点往四周扩散的同色块大小.

题目分析:

(初学链剖,戳开了这个题,然后……d了一天的bug)

在网上找到了一篇题解,感觉思路非常好,然后就理解了一发.

对于每个同色块,选择将同色块大小存放在其顶端结点处,那么每次询问操作的时候只需要找到顶端结点即可.
- 如何寻找顶端结点?
因为若两点之间无断点,是连续的,那么区间的同色点个数应当和区间长度一致,用一个树状数组维护.对于某一个点而言与其所在链顶端所成区间进行比较,若个数一致,那么断点不在此重链上,若个数比区间长度小,那么断点在此重链上,对重链进行二分,用相同的方式判断.

那么当结点颜色改变时,如何修改和维护?
先找到当前点x向上能影响到的最浅结点f,对于x的父节点直至f点的父节点都要修改,而非修改x到f.
- 不修改x点的原因
之后若x点的颜色再度反转,那么x点上方的一些点要修改的值就应该是x点原先下面的同色结点数,而若修改后,值就变成0了.
- 要修改f父节点的原因
因为若反转f点的颜色,f点是不会修改的,那么之后询问f点的值则应该是修改之前的值,若不修改,值是错误的.

代码:

#include<cstdio>
#include<iostream>
#include<algorithm>

using namespace std;

const int maxn=100000+10;
const int maxm=200000+10;

int read()
{
    char ch=getchar();int ret=0;
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') ret=ret*10+ch-'0',ch=getchar();
    return ret;
}

int fir[maxn],nxt[maxm],to[maxm],ecnt;

void add_edge(int u,int v)
{
    nxt[++ecnt]=fir[u];fir[u]=ecnt;to[ecnt]=v;
    nxt[++ecnt]=fir[v];fir[v]=ecnt;to[ecnt]=u;
}

int fa[maxn],sz[maxn],son[maxn];

void dfs1(int u,int p)
{
    fa[u]=p;sz[u]=1;son[u]=0;
    for(int i=fir[u];i;i=nxt[i]) if(to[i]!=p) {
        int v=to[i];
        dfs1(v,u);
        sz[u]+=sz[v];
        if(sz[v]>sz[son[u]]) son[u]=v;
    }
}

int top[maxn],dep[maxn],rnk[maxn],idx[maxn],id;//rnk 新编号映射旧编号 idx 旧编号映射新编号 

void dfs2(int u,int t,int d)
{
    top[u]=t;dep[u]=d;rnk[idx[u]=++id]=u;
    if(son[u]) dfs2(son[u],t,d+1);
    for(int i=fir[u];i;i=nxt[i])
        if(to[i]!=fa[u]&&to[i]!=son[u]) dfs2(to[i],to[i],d+1);
}

#define lowbit(x) (x&-x)

int col[maxn],T[3][maxn],n,q;//col:0 black,1 white 
//sum(T[0/1][u])表示u点向下的黑色/白色块大小 用于存放答案 
//sum(T[2][u])-sum(T[2][v])表示以u~v的黑色点个数 用于二分时判断区间内点颜色是否一致 

void add(int bit[],int x,int v)
{
    while(x<=n) bit[x]+=v,x+=lowbit(x);
}

int sum(int bit[],int x)
{
    int ret=0;
    while(x>0) ret+=bit[x],x-=lowbit(x);
    return ret;
}

int binary_search(int low,int up,int c)//low,up为新编号,low在下方,up在上方,二分查找某一重链上从哪个位置断开的 
{
    int mid,now,ret;
    while(up<=low) {
        mid=(low+up)>>1;
        now=sum(T[2],low)-sum(T[2],mid-1);
        if(c?now==0:now==low-mid+1) low=mid-1,ret=mid;
        else up=mid+1;
    }
    return rnk[ret];//返回值为ret映射的旧编号 
}

int find(int x)//找到从x向上能移动到的最浅位置,即所在同色块的顶端位置 
{
    int c=col[x],now;
    while(top[x]!=1) {
        now=sum(T[2],idx[x])-sum(T[2],idx[top[x]]-1);//计算重链的黑点个数 
        if(c?now==0:now==idx[x]-idx[top[x]]+1)//若该点为黑,且区间全黑;或者该点为白,且区间无黑 
            if(c==col[fa[top[x]]]) x=fa[top[x]];//若重链顶端父节点与x颜色一致,继续向上找 
            else return top[x];//否则能到达最浅位置即top[x]
        else return binary_search(idx[x],idx[top[x]],c);//否则区间存在断点,二分查找断点 
    }
    return binary_search(idx[x],idx[1],c);//x已在1所在重链上,二分查找断点 
}

void update(int x,int y,int v,int c)//树上两点间bit更新 
{
    while(top[x]!=top[y]) {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        add(T[c],idx[top[x]],v);
        add(T[c],idx[x]+1,-v);
        x=fa[top[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    add(T[c],idx[x],v);
    add(T[c],idx[y]+1,-v);
}

void solve(int x)//注意当前点不参与修改,find(x)的父节点也要修改,因为当该点也变换颜色时,是不会修改其本身的,所以在之前就应当先修改好 
{
    if(x>1) update(fa[find(x)],fa[x],-sum(T[col[x]],idx[x]),col[x]);//颜色变换前x上方能影响到的所有点进行修改 
    add(T[2],idx[x],(col[x]^=1)?-1:1);//修改T[2]中黑点个数 
    if(x>1) update(fa[find(x)],fa[x],sum(T[col[x]],idx[x]),col[x]);//颜色变换后x上方能影响到的所有点进行修改 
}

int main()
{
    n=read();
    for(int u,v,i=1;i<n;i++) {
        u=read(),v=read();
        add_edge(u,v);
    }
    dfs1(1,1);
    dfs2(1,1,1);
    for(int i=1;i<=n;i++) add(T[0],idx[i],sz[i]),add(T[0],idx[i]+1,-sz[i]),add(T[2],i,1);
    add(T[1],1,1);//白树中,初始化成1
    q=read();
    while(q--) {
        int op,x;
        op=read(),x=read();
        if(op) solve(x);
        else printf("%d\n",sum(T[col[x]],idx[find(x)]));
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值