bzoj1095: [ZJOI2007]Hide 捉迷藏(动态点分治)

题目大意:
给出一棵树,初始每个节点为黑色。
C操作改变一个节点的颜色。
A操作询问树上最远的两个黑色节点的距离。

这道题我也是学了好久。

解法:
我用到的是动态点分治,当然大牛们打LCT也是可以的(万能的LCT!!)
先普及一下树的重心(相信很多人都知道)
bzoj1095: [ZJOI2007]Hide 捉迷藏(动态点分治)_第1张图片
相对于上面这棵树,用f[i]表示删除i之后剩下的最大的子树的节点数(第一次听可能有点懵逼)
拿节点4来说,将4删除后剩下三棵子树。
(1,2,3)(5)(6,7,8)
最大的子树节点数量为3。
那么f[4]=3。
然后我们求出每一个节点的f值,让f最小的那个点作为这棵树的重心,目的就是使我们分出来的树更加平均,每一棵树处理的时间尽量的少。
(很明显这棵树的重心就是4了)

动态点分治就是基于树的重心上的解法。
将一棵树分成若干棵子树,对每一棵子树在进行分配,一直往下分直到每一颗树的节点数为1。
上一层的重心需要连接下一层的重心。
对这棵新树来进行分治递归求解。

首先普及一下堆(相信很多人都知道)
每一个堆都可以往里面放数字,放进去之后系统会自动从大到小(从小到大)排序。每次可以询问堆顶(堆里最大的值),可以删除堆顶,可以询问当前堆里有多少个数。
如果还不懂的同学自己上网查一查相关资料吧。

这道题的做法:
对于新树每个节点我们用两个堆来维护。
每个节点的第一个堆维护以自己为根的子树内所有节点到自己父亲节点的距离。
第二个堆维护每个子节点第一个堆的堆顶(就是每一个孩子节点的子树内里自己最远的距离)
那么相对于每一个节点,第二个堆的最大值加次大值就是子树内经过自己的最长链!!
对于全局:
开一个堆来维护每一个节点第二个堆的最大值加次大值,那么全局的堆顶就是答案!

这道题还需要求距离
我求树上距离用的是ST表。
没学过ST表的看这里
因为建完新树后父子关心全部都改变了。很显然不能在新树上求解距离。
那么就要在原树上求解距离。
求距离的方法有很多种。

我的方法:
dep[i]表示的是1到i节点的距离
那么x到y之间的距离=dep[x]+dep[y]-2*dep[最近公共祖先]
那么如何来求解dep[最近公共祖先]呢
我用的是dfs序上ST表优化区间最小值,当然用倍增LCA也是可以的。
显然x和y的最近公共祖先是x到y路径上深度最小的点。
如果我们用什么方法把路径上的点表示为连续的编号,
那么用ST表求区间最小值不就解决了吗。
所以想到了dfs序!
dfs序太难解释了,需要结合代码。

void dfs(int x,int fa)
{
    mn[0][++dfn]=dep[x];  //mn就是ST表的状态数组
    ys[x]=dfn;            //ys[x]表示就是x的dfs序编号。
    for(int k=last[x];k;k=a[k].next) {
        int y=a[k].y;
        if(y!=fa)
        {
            dep[y]=dep[x]+a[k].c;
            dfs(y,x);
            mn[0][++dfn]=dep[x]; //一定要给一个回溯,下面详细解释
        }
    }
}

解释一下上面的代码为什么要回溯。
下面展示一下不加回溯的情况
bzoj1095: [ZJOI2007]Hide 捉迷藏(动态点分治)_第2张图片
那么根据上图,要求x和y的最近公共祖先,答案明显是dfs序为2的点。
但是要求4到7的最小深度的点,答案很明显出来的是dfs序为5的点。
错了!
下面是加回溯的情况。
bzoj1095: [ZJOI2007]Hide 捉迷藏(动态点分治)_第3张图片
对于上图要求x和y的最近公共祖先。
那么4到9之间包括了4,5,6,7,8,9,很明显这就是x到y的路径。那么最小值肯定是最近公共祖先了!!

补充说明:
每个堆分成两个堆。因为变颜色的时候某些状态就不合法了。那么就需要删除。
但是堆每次只可以删除堆顶。
所以我们把每个堆分成两个堆,一个堆记录全部状态,一个堆记录没用的状态。
这样我们每次询问堆顶的时候,如果堆顶是没用的状态,那么我们就要删除。

代码实现(第一次做要做好心理准备)(会有充分的注释):

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define inf 1000000000
#define mod 1000000007
#define pa pair
#define ll long long 
using namespace std;
int bin[20],Log[200005];//bin[i]表示2的i次方,Log就是log
int n,m,G,cnt,dfn,sum,tott;
int tot[100005],f[100005],dep[100005];// f[i]数组表示去掉点i之后剩下所有子树家族最大的子树的家族数目 
//dep表示到一号节点的距离 
int mn[18][200005],ys[100005],fa[100005];//mn[i][j]表示dfs序中从j到j+2^i-1的最小深度的点 
bool v[100005],col[100005];//col表示当前这个点的颜色,1为黑色,0为白色 
struct node{
    int x,y,next;
}a[200005];int len,last[100005];
void ins(int x,int y) {
    len++;
    a[len].x=x;a[len].y=y;
    a[len].next=last[x];last[x]=len;
}
/*
做法。
对于每个节点我们维护两个堆(堆的堆顶最大)
每个节点的第一个堆维护所有子树内的节点到自己父亲节点的距离
第二个堆维护所有子节点第一个堆的堆顶(最大值)
那么相对于每一个节点来说,第二个堆的最大值加次大值就是子树内经过这个节点的最长链!!
全局维护一个堆,记录所有节点第二个堆的最大值和次大值的和。堆顶(最大值)就是答案 
*/ 
/*
堆的操作;
push(x),把x加进堆里
pop(),删除堆顶
top(),询问堆顶
size(),堆里有多少个数 
*/ 
struct heap{
    priority_queue<int> A,B;//A堆记录全部的状态,B堆记录没用的状态 
    void push(int x){  //加进一个目前有用的状态(将来可能没用了) 
        A.push(x);
    }
    void erase(int x){ //更好的理解为删除这个状态 
        B.push(x);
    }
    void pop(){  //删除堆顶 
        while(B.size()&&A.top()==B.top())  //首先要把没用的状态给删除了 
            A.pop(),B.pop();
        A.pop();  //去掉没用的状态后删除堆顶 
    }
    int top(){  //取堆顶 
        while(B.size()&&A.top()==B.top())// 删除没用的状态 
            A.pop(),B.pop();
        if(!A.size())return 0;  
        return A.top();
    }
    int size(){  //全部的状态数-没用的状态数=有用的状态数 
        return A.size()-B.size();
    }
    int stop(){  //求堆里的次大值 
        if(size()<2)return 0; //如果有用的状态数小于2说明没有次大值 
        int x=top();pop(); //先把堆顶删除 
        int y=top();push(x);//删除原来的堆顶之后剩下的堆顶就是原来的次大值!得到次大值之后再把原来的堆顶加回去 
        return y;
    }
}A,B[110000],C[110000];//C数组表示每个节点的第一个堆,B数组表示第二个堆,A表示全局的堆 
void dfs(int x,int fa) //求出dfs序和dep数组 
{
    mn[0][++dfn]=dep[x];
    ys[x]=dfn;
    for(int k=last[x];k;k=a[k].next) {
        int y=a[k].y;
        if(y!=fa)
        {
            dep[y]=dep[x]+1;
            dfs(y,x);
            mn[0][++dfn]=dep[x];
        }
    }
}
/*
getrt
找到树的重心并存在G里。
树的重心是一个点
删掉树的重心之后所剩子树家族最大的最小
*/
void getrt(int x,int fa)
{
    tot[x]=1;f[x]=0;
    for(int k=last[x];k;k=a[k].next) {
        int y=a[k].y;
        if(y!=fa&&v[y]==false)
        {
            getrt(y,x);
            tot[x]+=tot[y];
            f[x]=max(f[x],tot[y]);
        }
    }
    f[x]=max(f[x],sum-tot[x]); // 把下面的子树问完以后还有以父亲节点为根的那棵子树 
    if(f[x]// G记录的就是当前子树的重心 
        G=x; 
}
//按照新的树去建父子关系 
void divi(int x)
{
    v[x]=true;
    for(int k=last[x];k;k=a[k].next) {
        int y=a[k].y;
        if(v[y]==false)
        {
            sum=tot[y];G=0;   
            getrt(y,x); //找出这棵子树的重心 
            fa[G]=x;divi(G); //下一层的重心链接上一层的重心,然后继续去建新的树 
        }
    }
}
//求x到y之间深度最小的点的深度 
int rmq(int x,int y)
{
    x=ys[x];y=ys[y];
    if(x>y)
        swap(x,y);
    int t=Log[y-x+1];
    return min(mn[t][x],mn[t][y-bin[t]+1]);//ST表求解公式 
}

int dis(int x,int y) //原树上x到y的距离 
{
    return dep[x]+dep[y]-2*rmq(x,y);
}
//每个节点的堆都是维护下面的节点。
//那么改变一个节点的颜色就只会对它上面的节点有影响。 
//把v节点关上,v对u节点有影响。 
void turn_off(int u,int v)
{
    if(u==v)
    {
        B[u].push(0);
        if(B[u].size()==2)A.push(B[u].top());
    }
    if(!fa[u])return;
    int f=fa[u],D=dis(f,v),tmp=C[u].top();
    C[u].push(D);//把v节点改为黑色,那么这个距离一定是有用的。 
    if(D>tmp) //如果现在的距离优于原来的最大距离。那么有可能对我的答案有影响。 
    {
        int mx=B[f].top()+B[f].stop(),size=B[f].size();//mx记录原来的答案 
        if(tmp)B[f].erase(tmp);//tmp已经不是堆顶了,不能被记录在B[f]里。要删除 
        B[f].push(D);          //D成为了堆顶 ,要被记录在B[f]里。 
        int now=B[f].top()+B[f].stop(); //当前的答案 
        if(now>mx)  //如果当前答案优于原来的答案 
        {
            if(size>=2)A.erase(mx);  //size>=2说明你有最大值和次大值,mx已经不是最优答案了,要把他删除 
            if(B[f].size()>=2)A.push(now);//now是当前最优答案,要记录进全局堆 
        }
    }
    turn_off(f,v);// 往上更新 
}
//把v节点打开,v对于u有影响 
void turn_on(int u,int v)//跟turn_off思想差不多 
{
    if(u==v)
    {
        if(B[u].size()==2)A.erase(B[u].top());
        B[u].erase(0);    
    }
    if(!fa[u])return;
    int f=fa[u],D=dis(f,v),tmp=C[u].top();
    C[u].erase(D);//D肯定没有用了,因为v变成了白色 
    if(D==tmp)//如果我等于原来的堆顶,那么有可能删掉的就是堆顶(堆顶有可能有多个重复),那么就有可能对我的答案有影响 
    {
        int mx=B[f].top()+B[f].stop(),size=B[f].size();
        B[f].erase(D);//很明显D没用了要删除。 
        if(C[u].top())B[f].push(C[u].top());//如果你的堆顶不为零,那么我要记录一下。 
        int now=B[f].top()+B[f].stop();//现在的答案 
        if(now//原来的答案经过更新之后变小了,说明改变v是对f的答案有影响的,那么我要更新全局 
        {
            if(size>=2)A.erase(mx); // 不解释了 
            if(B[f].size()>=2)A.push(now);
        }
    }
    turn_on(f,v);
}
int main()
{
    bin[0]=1;for(int i=1;i<20;i++)bin[i]=bin[i-1]<<1;
    Log[0]=-1;for(int i=1;i<=200000;i++)Log[i]=Log[i>>1]+1;
    scanf("%d",&n);
    len=0;memset(last,0,sizeof(last));
    for(int i=1;iint x,y;scanf("%d%d",&x,&y);
        ins(x,y);ins(y,x);
    }
    dfs(1,0);
    // mn[i][j]表示j到j+2^i-1最小深度的点
    // ST表模板 
    for(int i=1;i<=Log[dfn];i++)
        for(int j=1;j<=dfn;j++)
            if(j+bin[i]-1<=dfn)
                mn[i][j]=min(mn[i-1][j],mn[i-1][j+bin[i-1]]);
    G=0;f[0]=inf;sum=n;
    getrt(1,0);
    fa[G]=0;divi(G);
    for(int i=1;i<=n;i++)col[i]=1;//col[i]=1时表示该节点为关着的,=0时为开着的。
    for(int i=1;i<=n;i++)
    {
        turn_off(i,i);
        tott++;//tott表示关灯房间数量数量 
    }
    char ch[2];
    scanf("%d",&m);
    while(m--)
    {
        scanf("%s",ch+1);
        if(ch[1]=='G')
        {
            if(tott<=1)printf("%d\n",tott-1);
            else printf("%d\n",A.top());
        }
        else
        {
            int x;scanf("%d",&x);
            if(col[x])turn_on(x,x),tott--;
            else turn_off(x,x),tott++;
            col[x]^=1;
        }
    }
    return 0;
}

这篇博客写死我。希望能对第一次做这题的人有所帮助。
最后来说一下时间复杂度。用树的重心进行分治使得新树的层数不会超过logn层。
用堆来维护,时间复杂度也是logn层。
总的时间复杂度O(logn^2)
如果不喜欢用queue,也可以手写堆(但要珍惜生命啊)
做完这道题我感觉还是受益匪浅啊。学到了很多新的知识。
绝对的经典好题!

你可能感兴趣的:(ST表,动态树)