基环树(环套树)

· · 个人记录

基环树

基环树也叫环套树,就是n个点n条边的连通图,可以发现只有一个环,并且删掉环上任意一个边可以变成一棵树。如果不联通,那么就是基环树森林。

基环树一般分成环和树来分别处理(显然环的处理较为麻烦),那首先得找到环.

找环

  1. dfs

    int fa[maxn],dfn[maxn],loop[maxn],cnt,idx;
    void get_lop(int x){
        dfn[x]=++idx;
        for(int i=head[x];i;i=Next[i]){
            if(ver[i]==fa[x]) continue;
            if(dfn[ver[i]]){
                if(dfn[ver[i]]<dfn[x]) continue;
                loop[++cnt]=ver[i];
                int y=ver[i];
                while(y!=x){
                    loop[++cnt]=fa[y];
                    y=fa[x];
                }
            }else{
                fa[ver[i]]=x;
                get_lop(ver[i]);
            }
        }
    }
  2. 拓扑排序

    void topsort(){
        queue<int> q;
        for(int i=1;i<=n;++i) if(in[i]==1) q.push(i);
        while(q.size()){
            int x=q.front();
            q.pop();
            for(int i=he[x];i;i=nx[i]){
                if(in[vr[i]]>1){
                    if(--in[vr[i]]==1) {
                        q.push(vr[i]);
                    }
                }
            }
        }
    }

骑士(ZJOI2008)

题目大意

求各个基环树的最大攻击力之和

可以对每棵基环树分别进行树上dp,类似没有上司的舞会那道题

#include <iostream> 
using namespace std;
int n;
long long a[1000006];
int nx[1000006],he[1000006],vr[1000006],tot;
inline void add(int x,int y){
    vr[++tot]=y;
    nx[tot]=he[x];
    he[x]=tot;
}
bool v[1000006];
int fa[1000006],root;
long long f[1000006][2],ans;
void dp(int x){
    v[x]=1;
    f[x][0]=0;
    f[x][1]=a[x];
    for(int i=he[x];i;i=nx[i]){
        if(vr[i]!=root){
            dp(vr[i]);
            f[x][0]+=max(f[vr[i]][0],f[vr[i]][1]);
            f[x][1]+=f[vr[i]][0];
        }else{
            f[vr[i]][1]=-0x3f3f3f3f;
        }
    } 
}
void find_circle(int x){
    v[x]=1;
    root=x;
    while(!v[fa[root]]){
        root=fa[root];
        v[root]=1;
    }
    dp(root);
    long long res=max(f[root][0],f[root][1]);
    v[root]=1;
    root=fa[root];
    dp(root);
    ans+=max(res,max(f[root][0],f[root][1]));
} 
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;int x;
    for(int i=1;i<=n;++i){
        cin>>a[i];
        cin>>x;
        add(x,i);
        fa[i]=x;
    }
    for(int i=1;i<=n;++i){
        if(!v[i]){
            find_circle(i);
        }
    }
    cout<<ans<<endl;
    return 0;
}

基环树的直径

定义

基环树中最长的简单路径被称为基环树的最长链,其长度被称为基环树的直径。这里的简单路径指的是不自交的(不重复经过任何点或边)路径

Island(IOI2008)

题目大意

求各个基环树的直径和

先用dfs或拓扑排序找到基环树的环,把环上的点标记,再对每个点进行dfs,就可以访问以该点为根的子树。 在每棵子树中求出直径。设f[x]表示以x为根的子树的直径。设i,j为环上两点,d[i]表示环上边权的前缀和。 那么基环树的直径就是f[i]+f[j]+d[j]-d[i],其中d[j]-d[i]i,j两点的在环上的距离。对于d数组,可以把环断开复制一遍,枚举j,用单调队列维护f[i]-d[i]的最大值. 时间复杂度:O(n)

#include <iostream> 
#include <queue>
using namespace std;
int n;
int nx[2000006],he[1000006],vr[2000006],eg[2000006],tot;
inline void add(int x,int y,int z){
    vr[++tot]=y;
    eg[tot]=z;
    nx[tot]=he[x];
    he[x]=tot;
}
int in[1000006],ku[1000006],v[1000006],t,q[2000006];
long long ans,d[1000006],f[1000006],a[1000006],b[1000006];
// 一定要开long long !!!
void topsort(){
    queue<int> q;
    for(int i=1;i<=n;++i) if(in[i]==1) q.push(i);
    while(q.size()){
        int x=q.front();
        q.pop();
        for(int i=he[x];i;i=nx[i]){
            if(in[vr[i]]>1){
                d[ku[x]]=max(d[ku[x]],f[x]+f[vr[i]]+eg[i]);
                f[vr[i]]=max(f[vr[i]],f[x]+eg[i]);// 树的直径
                if(--in[vr[i]]==1) {
                    q.push(vr[i]);
                }
            }
        }
    }
}
void dp(int k,int x){
    int m=0;// 环上点的数量
    int y=x,z=0,len=0;
    do{
        a[++m]=f[y];// 记录以环上各点为根的子树的直径
        in[y]=1;
        for(z=he[y];z;z=nx[z]){
            if(in[vr[z]]>1){
                y=vr[z];
                b[m+1]=b[m]+eg[z];// 标记环上的点
                break;
            }
        }
    }while(z);
    if(m==2){
        for(int i=he[y];i;i=nx[i]){
            if(vr[i]==x){
                len=max(len,eg[i]);
            }
        }
        d[k]=max(d[k],f[x]+f[y]+len);
        return;
    }
    for(int i=he[y];i;i=nx[i]){
        if(vr[i]==x){
            b[m+1]=b[m]+eg[i];
            break;
        }
    }
    for(int i=1;i<m;++i){
        a[i+m]=a[i];b[i+m]=b[m+1]+b[i]; // 复制
    }
    int l,r;
    q[l=r=1]=1;// 单调队列
    for(int i=2;i<(m<<1);++i){
        while(l<=r && i-q[l]>=m) ++l;
        d[k]=max(d[k],a[q[l]]+a[i]+b[i]-b[q[l]]);
        while(l<=r && a[q[r]]-b[q[r]]<=a[i]-b[i]) --r;
        q[++r]=i;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    int x,z;
    for(int i=1;i<=n;++i){
        cin>>x>>z;
        add(x,i,z);
        add(i,x,z);
        ++in[x];++in[i];
    }
    queue<int> q;
    for(int i=1;i<=n;++i){ // 遍历每个连通块
        if(ku[i]) continue;
        while(q.size()) q.pop();
        q.push(i);
        ku[i]=++t;
        while(q.size()){
            x=q.front();
            q.pop();
            for(int i=he[x];i;i=nx[i]){
                if(ku[vr[i]]) continue;
                q.push(vr[i]);
                ku[vr[i]]=t;
            }
        }
    }
    topsort();// 拓扑排序
    for(int i=1;i<=n;++i){
        if(in[i]>1 && !v[ku[i]]) { // 如果该点入度大于1,那么就是环上的点
            v[ku[i]]=1;// 标记
            dp(ku[i],i);
            ans+=d[ku[i]];// 统计每个基环树最长简单路径
        }
    }
    cout<<ans;
    return 0;
}