基环树

基环树

概念: n 个点 n 条边组成的图,比树多出一个环,有且只有一个环
外向树:每个点只有一条入边(in>=out 且 out=1)
内向树:每个点只有一条出边(in<=out 且 in=1)

练习题

P4381 [IOI2008]Island

链接:https://www.acwing.com/problem/content/description/360/

题意:给定一个基环树森林,求所有基环树的直径之和

思路: 对每个联通块分别操作。设 d i s 1 [ u ] dis1[u] dis1[u] 为以 u 为根的子树到叶的最远距离。 d i s 2 [ u ] dis2[u] dis2[u]为环上起点到 u 的距离。

  • 先 dfs 联通块,让度为 0 的点,入队
  • 然后用拓扑排序剥离这些点,剩下没访问过的就是环上的点。拓扑排序的同时,可以统计单棵树内的答案。
  • 然后对环统计答案, a n s = m a x ( d i s 1 [ i ] + d i s 1 [ j ] + d i s 2 [ i ] − d i s 2 [ j ] ) ans=max(dis1[i]+dis1[j]+dis2[i]-dis2[j]) ans=max(dis1[i]+dis1[j]+dis2[i]dis2[j]),因此只需要维护一个长度为 n 的滑动窗口,用单调递减队列维护 d i s 1 [ j ] − d i s 2 [ j ] dis1[j]-dis2[j] dis1[j]dis2[j]。这里,对于一个 i :j 的取值范围为: [ i − n + 1 , i − 1 ] [i-n+1,i-1] [in+1,i1],因此,需要先统计答案之后,才能够将第 i 个点入队。(这里 wa 了很久)
#include <bits/stdc++.h>
#define ll long long
#define val(i) (a[i]-b[i])
using namespace std;
const int maxn=1e6+10;

struct Edge
{
    int nxt,to,w;
} edges[maxn<<1];
int head[maxn],ecnt;
void add(int u,int v,int w)
{
    edges[++ecnt]= {head[u],v,w};
    head[u]=ecnt;
}
bool vis1[maxn],vis2[maxn];
ll dis1[maxn],dis2[maxn],a[maxn<<1],b[maxn<<1];
int id[maxn],in[maxn],times;
queue<int> q;

void dfs1(int u)
{
    vis1[u]=1;
    if(in[u]==1) q.push(u);

    for(int i=head[u]; i!=-1; i=edges[i].nxt)
    {
        int v=edges[i].to;
        if(vis1[v]) continue;
        dfs1(v);
    }
}
pair<ll,ll> topo()
{
    ll ans=0,x;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        vis2[u]=1;
        for(int i=head[u]; i!=-1; i=edges[i].nxt)
        {
            int v=edges[i].to,w=edges[i].w;
            if(vis2[v]) continue;
            ans=max(ans,dis1[u]+dis1[v]+w);
            dis1[v]=max(dis1[v],dis1[u]+w);
            in[v]--;
            if(in[v]==1) q.push(v);
            else x=v;
        }
    }
    return {ans,x};
}
void dfs2(int u)
{
    vis2[u]=1;
    id[++times]=u;
    for(int i=head[u]; i!=-1; i=edges[i].nxt)
    {
        int v=edges[i].to,w=edges[i].w;
        if(vis2[v]) continue;
        dis2[v]=dis2[u]+w;
        dfs2(v);
    }
}
ll solve(int x)
{
    ll ans=0;
    if(!q.empty())
    {
        auto res=topo();
        ans=max(ans,res.first);
        x=res.second;
    }
    times=0;
    dfs2(x);
    if(times==2)
    {
        int u=id[1];
        for(int i=head[u]; i!=-1; i=edges[i].nxt)
        {
            int v=edges[i].to,w=edges[i].w;
            if(v==id[2]) ans=max(ans,dis1[u]+dis1[v]+w);
        }
        return ans;
    }

    int u=id[times];
    ll len=dis2[u];
    if(times>2)
    {
        for(int i=head[u]; i!=-1; i=edges[i].nxt)
        {
            int v=edges[i].to;
            if(v==id[1])
            {
                len+=edges[i].w;
                break;
            }
        }
    }
    int n=times;
    for(int i=1; i<=n; ++i)
    {
        int u=id[i];
        a[i]=a[i+n]=dis1[u];
        b[i]=dis2[u];
        b[i+n]=dis2[u]+len;
    }
    int qq[maxn<<1],h=0,t=-1;
    for(int i=1; i<=2*n; ++i)
    {
        while(h<=t&&i-qq[h]>=n) h++;
        if(h<=t) ans=max(ans,a[i]+b[i]+val(qq[h]));
        while(h<=t&&val(qq[t])<=val(i)) t--;
        qq[++t]=i;
    }
    return ans;
}
int main()
{
    int n;
    scanf("%d",&n);
    ecnt=0;
    memset(head,-1,sizeof(head));
    for(int i=1; i<=n; ++i)
    {
        int v,w;
        scanf("%d%d",&v,&w);
        add(i,v,w),add(v,i,w);
        in[i]++,in[v]++;
    }
    ll ans=0;
    for(int i=1; i<=n; ++i)
    {
        if(vis1[i]) continue;
        dfs1(i);
        ans+=solve(i);
    }
    printf("%lld\n",ans);
    return 0;
}
#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
long long ans,res,f[N],v1[N],v2[N],u1[N],u2[N];
int dfn[N],fa[N],pre[N],a[N],b[N],cnt;
int n,x,w,tot,edge[N<<1],ver[N<<1],Next[N<<1],head[N<<1];
bool vis[N];
void add(int u,int v,int w)//加边
{
    edge[++cnt]=v;
    ver[cnt]=w;
    Next[cnt]=head[u];
    head[u]=cnt;
}

void dfs(int x)//找到基环
{
    dfn[x]=++cnt;//dfs序
    for(int i=head[x]; i; i=Next[i])//遍历所有出边
    {
        int k=edge[i];//终边
        if (k==fa[x])//不能到父亲节点
            continue;
        if (!dfn[k])//出边没有DFS序,访问
            fa[k]=x,pre[k]=ver[i],dfs(k);//那么k的父亲是当前节点,而且pre存储边
        else if (dfn[k]>dfn[x])//如果发现k已经访问了,而且在x之前,说明出现基环
        {
            for (int t=k; t!=x; t=fa[t])//访问基环路径
                a[++tot]=t,vis[t]=1,b[tot]=pre[t];
            a[++tot]=x;//存储基环节点
            vis[x]=1;//已经访问
            b[tot]=ver[i];//存储基环的边
        }
    }
}
void Dfs(int x,int fa)//求每一棵DFS树的直径
{
    for(int i=head[x]; i; i=Next[i])//访问所有出边
    {
        int k=edge[i];
        if (k==fa || vis[k])//如果已经访问了,或者回到父亲了
            continue;
        Dfs(k,x);
        ans=max(ans,f[x]+f[k]+ver[i]);//最长链+次长链
        f[x]=max(f[x],f[k]+ver[i]);
    }
}

void DP(int x)
{
    tot=ans=0;
    dfs(x);
    long long sm=0,mx=0;
    for(int i=1; i<=tot; i++)//一个一个遍历
        Dfs(a[i],0);
    for(int i=1; i<=tot; i++)//初始化
        vis[a[i]]=0;
    for(int i=0; i<=tot+1; i++)//初始化
        u1[i]=u2[i]=v1[i]=v2[i]=0;
    for(int i=1; i<=tot; i++)
    {
        sm+=b[i-1];//前缀和,基环上的边 
        u1[i]=max(u1[i-1],f[a[i]]+sm);//第二类情况,也就是基环树的两个环上的点 
        v1[i]=max(v1[i-1],f[a[i]]+sm+mx);//第一类情况,也就是基环上的点,为子树的树的直径 
        mx=max(mx,f[a[i]]-sm);//第一类要补上 
        ans=max(ans,v1[i]);//第一类存储 
    }
    int tmp=b[tot];
    sm=mx=b[tot]=0;//初始化
    for (int i=tot; i; i--)//所谓的拆环成链,其实就是反过来一遍
    {
        sm+=b[i];//前缀和 
        u2[i]=max(u2[i+1],f[a[i]]+sm);
        v2[i]=max(v2[i+1],f[a[i]]+sm+mx);
        mx=max(mx,f[a[i]]-sm);
        ans=max(ans,v2[i]);
    }
    for(int i=1; i<tot; i++)
        ans=max(ans,u1[i]+u2[i+1]+tmp);
    res+=ans;
}
int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
        scanf("%d%d",&x,&w),add(x,i,w),add(i,x,w);//加边
    for(int i=1; i<=n; i++)
        if (!dfn[i])//遍历每一棵基环树
            DP(i);
    printf("%lld\n",res);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值