P4381 [IOI2008]Island

题意:

给你一棵基环树森林,求出基环树的直径之和.

题解:

对于基环树,我们将环看作根,那么直径有两种情况::

1.不经过环,也就是环上某个点的子树内部,对于这种情况,直接在子树内部处理直径,更新答案即可;

2.经过环,答案就是i的子树内长度+j的子树内长度+i和j之间的距离,我们预处理出环上每个点到其子树上的最长距离,在预处理一个环上前缀和,ans=max{sondis[i]+sondis[j]+sumcircle[i]-sumcircle[j]},更新答案即可.
sondis[i]记录环上的第i个点到其子树的最远距离;
sumcircle[]记录环上距离前缀和;
sumcircle[i]-sumcircle[j]即为i和j的距离

代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int N=1e6+3;
int to[N<<1],nex[N<<1],head[N],w[N<<1],tt;
int c[N];//记录当前节点属于第几棵基环树;
int dg[N];//记录度数;
int q[N<<1];//数组模拟队列;
ll d[N];//d[i]记录第i棵树上的直径;
ll f[N];//f[i]记录从i点向儿子方向上所可以走的最长距离;
ll sumcircle[N<<1];//环上距离前缀和;
ll sondis[N<<1];//sondis[i]记录环上的第i个点到其子树的最远距离;
int cnt,n;
bool vis[N];//标记当前基环树是否已解决过;
inline void add(int x,int y,int W){
    to[++tt]=y,w[tt]=W,nex[tt]=head[x],head[x]=tt;
    return ;
}
inline void bfs(int p,int t){
    int l=0,r=1;
    q[1]=p,c[p]=t;//广搜预处理每一个点所属基环树的编号;
    while(l<r){
        ++l;
        int x=q[l];
        for(int i=head[x],v;i;i=nex[i]){
            v=to[i];
            if(!c[v]){
                q[++r]=v;
                c[v]=t;
            }
        }
    }
    return ;
}
inline void top_sort(){//拓扑排序求不经过环的直径,从叶子节点向根(环)遍历;
    int l=0,r=0;
    for(int i=1;i<=n;++i)if(dg[i]==1)q[++r]=i;//将所有度数为1的点先加入队列中(即叶子结点);
    while(l<r){
        ++l;
        int x=q[l];
        for(int i=head[x],v;i;i=nex[i]){
            v=to[i];
            if(dg[v]>1){//此时v在以环为根的树上是其实是x的父亲;
                d[c[x]]=max(d[c[x]],f[x]+f[v]+w[i]);//更新当前基环树上的答案;
                f[v]=max(f[v],f[x]+w[i]);//更新父节点可以到达的最远节点;
                if(--dg[v]==1)q[++r]=v;//若当前度数现在变为1,入队;
            }
        }
    }
    return ;
}
inline void work(int t,int x){
    int tot=0,l,r,v=x,u,i;//在拓扑排序后已经将非环上的边遍历完了,环上的边和点均还未遍历,且度数为2,将x看做环上的起点,v看做终点;
    do{
        dg[v]=1;
        sondis[++tot]=f[v];//将当前点度数记为1,防止反向遍历,同时记录第tot号环上节点到子树上的最短距离;
        for(i=head[v];i;i=nex[i]){
            u=to[i];
            if(dg[u]>1){//环上的点度数均大于1,所以度数大于1的点即是环上的点;
                v=u;//更新终点;
                sumcircle[tot+1]=sumcircle[tot]+w[i];//处理环上距离前缀和;
                break;//保证向一边枚举,所以找到一个点就break;
            }
        }
    }while(i);
    if(tot==2){//特殊处理二元环;
        int len=0;
        for(i=head[v];i;i=nex[i]){
            u=to[i];
            if(u==x){
                len=max(len,w[i]);//找出两个点之间较大的边; 
            }
        }
        d[t]=max(d[t],(ll)len+f[x]+f[v]);//更新答案;
        return ;
    }
    for(int i=head[v],u;i;i=nex[i]){
        u=to[i];
        if(u==x){
            sumcircle[tot+1]=sumcircle[tot]+w[i];//将起点和终点的距离处理出来;
            break;
        }
    }
    for(i=1;i<tot;i++)//断环为链,再复制一遍数组;
    {
        sondis[tot+i]=sondis[i];
        sumcircle[tot+i]=sumcircle[tot+1]+sumcircle[i];
    }
    q[1]=1;
    l=r=1;
    for(int i=2;i<tot<<1;++i){
        while(l<=r&&i-q[l]>=tot)++l;//将超出环长度的部分弹出;
        d[t]=max(d[t],sondis[q[l]]+sondis[i]+sumcircle[i]-sumcircle[q[l]]);
        while(l<=r&&sondis[q[r]]+sumcircle[i]-sumcircle[q[r]]<=sondis[i])--r;//将队尾答案不够优秀的部分弹出;
        q[++r]=i;//入队;
    }
    return ;
}
int main(){

    cin>>n;
    int x,W;
    for(int i=1;i<=n;++i)
    {
        scanf("%d%d",&x,&W);
        add(x,i,W);
        add(i,x,W);
        ++dg[x];
        ++dg[i];
    }
    for(int i=1;i<=n;++i)if(!c[i])bfs(i,++cnt);//预处理;
    top_sort();//拓扑排序求直径;
    ll ans=0;
    for(int i=1;i<=n;++i)
        if(dg[i]>1&&!vis[c[i]]){//如果当前基环树未被处理,并且当前点是环上点,就处理;
            vis[c[i]]=true;
            work(c[i],i);
            ans+=d[c[i]];//累加答案;
        }
    cout<<ans<<'\n';
    return 0;
}