【学习笔记】基环树

基环树

无向图

  • 若一个无向连通图有 \(n\) 个点和 \(n\) 条边,即是在树上加一条边后构成的图中恰好包含一个环的图,则称它是一棵基环树。
  • 若一个无向图有 \(n\) 个点和 \(n\) 条边,即是由若干棵基环树组成的森林,则称它是一个基环树森林。

有向图

  • 若一个有向连通图有 \(n\) 个点和 \(n\) 条边,每个节点有且仅有一条入边,则称它是一棵基环外向树。
  • 若一个有向连通图有 \(n\) 个点和 \(n\) 条边,每个节点有且仅有一条出边,则称它是一棵基环内向树。
  • 基环内向树和基环外向树统称基环树。
  • 若一个有向图有 \(n\) 个点和 \(n\) 条边,每个节点有且仅有一条出(入)边,即是由若干棵基环外(内)向树组成的森林,则称它是一个基环外(内)向树森林。

例题

luogu P8655 [蓝桥杯 2017 国 B] 发现环

  • 基环树找环板子。

  • \(DFS+\) 并查集

    点击查看代码
  • \(Tarjan\)

    点击查看代码
    struct node
    {
        int nxt,to;
    }e[200001];
    int head[200001],dfn[200001],low[200001],ins[200001],c[200001],cnt=0,tot=0,ans=0,rt=0;
    vector<int>scc[200001];
    stack<int>s;
    void add(int u,int v)
    {
        cnt++;
        e[cnt].nxt=head[u];
        e[cnt].to=v;
        head[u]=cnt;
    }
    void tarjan(int x,int fa)
    {
        int i,k=0;
        tot++;
        dfn[x]=low[x]=tot;
        ins[x]=1;
        s.push(x);
        for(i=head[x];i!=0;i=e[i].nxt)
        {
            if(e[i].to!=fa)
            {
                if(dfn[e[i].to]==0)
                {
                    tarjan(e[i].to,x);
                    low[x]=min(low[x],low[e[i].to]);
                }
                else
                {
                    if(ins[e[i].to]==1)
                    {
                        low[x]=min(low[x],dfn[e[i].to]);
                    }
                }
            }
        }
        if(dfn[x]==low[x])
        {
            ans++;
            while(x!=k)
            {
                k=s.top();
                ins[k]=0;
                c[k]=ans;
                scc[ans].push_back(k);
                s.pop();
            }
            if(scc[ans].size()>=2)
            {
                rt=ans;
            }
        }
    }
    int main()
    {
        int n,i,u,v;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            cin>>u>>v;
            add(u,v);
            add(v,u);
        }
        for(i=1;i<=n;i++)
        {
            if(dfn[i]==0)
            {
                tarjan(i,0);
            }
        }
        sort(scc[rt].begin(),scc[rt].end());
        for(i=0;i<scc[rt].size();i++)
        {
            cout<<scc[rt][i]<<" ";
        }
        return 0;
    }
    
  • 拓扑排序

    点击查看代码

CF131D Subway

luogu P2921 [USACO08DEC] Trick or Treat on the Farm G

  • 判断是否有自环。若没有则为基环内向树。

  • 若节点 \(x\) 在环上,则答案为环的大小。

  • 若节点 \(x\) 不在环上,则答案为节点 \(x\) 到环的距离 \(+\) 环的大小。

    点击查看代码
    struct node
    {
        int next,to;
    }e[100010];
    stack<int>s;
    int head[100010],dfn[100010],low[100010],ins[100010],scc[100010],c[100010],u[100010],v[100010],dis[100010],cnt=0,tot=0,ans=0;
    void add(int u,int v)
    {
        cnt++;
        e[cnt].next=head[u];
        e[cnt].to=v;
        head[u]=cnt;
    }
    void tarjan(int x)
    {
        int i,k=0;
        tot++;
        dfn[x]=low[x]=tot;
        ins[x]=1;
        s.push(x);
        for(i=head[x];i!=0;i=e[i].next)
        {
            if(dfn[e[i].to]==0)
            {
                tarjan(e[i].to);
                low[x]=min(low[x],low[e[i].to]);
            }
            else
            {
                if(ins[e[i].to]==1)
                {
                    low[x]=min(low[x],dfn[e[i].to]);
                }
            }
        }
        if(dfn[x]==low[x])
        {
            ans++;
            while(x!=k)
            {
                k=s.top();
                ins[k]=0;
                c[k]=ans;
                scc[ans]++;
                s.pop();
            }
        }
    }
    void dfs(int rt,int x,int sum)
    {
        if(dis[x]==0)
        {
            dfs(rt,v[x],sum+1);
        }
        else
        {
            dis[rt]=dis[x]+sum;
        }
    }
    int main()
    {
        int n,i,j,sum;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            u[i]=i;
            cin>>v[i];
            add(u[i],v[i]);
        }
        for(i=1;i<=n;i++)
        {
            if(dfn[i]==0)
            {
                tarjan(i);
            }
        }
        for(i=1;i<=n;i++)
        {
            if(u[i]==v[i])
            {
                dis[i]=1;
            }
            else
            {
                dis[i]=(scc[c[u[i]]]>=2)*scc[c[u[i]]];
            }
        }
        for(i=1;i<=n;i++)
        {
            if(dis[i]==0)
            {
                dfs(u[i],v[i],1);
            }
        }
        for(i=1;i<=n;i++)
        {
            cout<<dis[i]<<endl;
        }
        return 0;
    }
    

luogu P2607 [ZJOI2008] 骑士

  • 将自己最痛恨的骑士向自己连一条有向边,这样就形成了基环外向树森林。

  • 基环外向树森林内每棵基环外向树是相互独立的,需要单独处理。

  • 对于每棵基环外向树,任取环上一点 \(x\) ,断开 \(x\)\(fa_{x}\) 的有向边,外向树就变成了一棵以 \(x\) 为根的树。

  • \(f_{x,0/1}\) 表示 \(x\) 不出征/出征时,以 \(x\) 为根的子树的最大战斗力,状态转移方程为 \(\begin{cases} f_{x,0}=\sum\limits_{y \in Son(x)}\max(f_{y,0},f_{y,1}) \\ f_{x,1}=w_{x}+\sum\limits_{y \in Son(x)}f_{y,0} \end{cases}\)

  • 将原问题拆成两部分。第一部分为 \(x\) 不限制 \(fa_{x}\) ,断开 \(x\)\(fa_{x}\) 的有向边,在以 \(x\) 为根的树上进行 \(DP\) ,得到的结果为 \(\max(f_{x,0},f_{x,1})\) ;第二部分为 \(x\) 限制 \(fa_{x}\) 。故再次以 \(fa_{x}\) 为根进行 \(DP\) ,得到的结果为 \(f_{fa_{x},0}\) 。这两部分合起来能够覆盖整个问题,故二者取 \(\max\) 即可。

    点击查看代码
    struct node
    {
        ll nxt,to;
    }e[1000010];
    ll head[1000010],vis[1000010],u[1000010],v[1000010],w[1000010],f[1000010][2],rt,cnt=0;
    void add(ll u,ll v)
    {
        cnt++;
        e[cnt].nxt=head[u];
        e[cnt].to=v;
        head[u]=cnt;
    }
    ll dfs_huan(ll x)
    {
        vis[x]=1;
        return (vis[u[x]]==1)?x:dfs_huan(u[x]);
    }
    void dfs(ll x)
    {
        vis[x]=1;
        f[x][0]=0;
        f[x][1]=w[x];
        for(ll i=head[x];i!=0;i=e[i].nxt)
        {
            if(e[i].to==rt)
            {
                f[e[i].to][1]=-0x3f3f3f3f;
            }
            else
            {
                dfs(e[i].to);
                f[x][0]+=max(f[e[i].to][0],f[e[i].to][1]);
                f[x][1]+=f[e[i].to][0];
            }
        }
    }
    int main()
    {
        ll n,ans=0,maxx,i;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            v[i]=i;
            cin>>w[i]>>u[i];
            add(u[i],v[i]);
        }
        for(i=1;i<=n;i++)
        {
            if(vis[i]==0)
            {
                rt=dfs_huan(i);
                dfs(rt);
                maxx=max(f[rt][0],f[rt][1]);
                rt=u[rt];
                dfs(rt);
                ans+=max(maxx,f[rt][0]);
            }
        }
        cout<<ans<<endl;
        return 0;
    }
    

luogu P1453 城市环路

  • 处理下无向图中的环即可。

    点击查看代码
    struct node
    {
        ll nxt,to;
    }e[200010];
    ll head[200010],vis[200010],dfn[200010],low[200010],ins[200010],c[200010],w[200010],f[200010][2],id,rt,cnt=0,tot=0,ans=0;
    vector<ll>scc[200010];
    stack<ll>s;
    void add(ll u,ll v)
    {
        cnt++;
        e[cnt].nxt=head[u];
        e[cnt].to=v;
        head[u]=cnt;
    }
    void tarjan(int x,int fa)
    {
        int i,k=0;
        tot++;
        dfn[x]=low[x]=tot;
        ins[x]=1;
        s.push(x);
        for(i=head[x];i!=0;i=e[i].nxt)
        {
            if(e[i].to!=fa)
            {
                if(dfn[e[i].to]==0)
                {
                    tarjan(e[i].to,x);
                    low[x]=min(low[x],low[e[i].to]);
                }
                else
                {
                    if(ins[e[i].to]==1)
                    {
                        low[x]=min(low[x],dfn[e[i].to]);
                    }
                }
            }
        }
        if(dfn[x]==low[x])
        {
            ans++;
            while(x!=k)
            {
                k=s.top();
                ins[k]=0;
                c[k]=ans;
                scc[ans].push_back(k);
                s.pop();
            }
            if(scc[ans].size()>=2)
            {
                id=ans;
            }
        }
    }
    void dfs(ll x,ll fa)
    {
        vis[x]=1;
        f[x][0]=0;
        f[x][1]=w[x];
        for(ll i=head[x];i!=0;i=e[i].nxt)
        {
            if(e[i].to!=fa)
            {
                if(e[i].to==rt)
                {
                    f[e[i].to][1]=-0x3f3f3f3f;
                }
                else
                {
                    dfs(e[i].to,x);
                    f[x][0]+=max(f[e[i].to][0],f[e[i].to][1]);
                    f[x][1]+=f[e[i].to][0];
                }
            }
        }
    }
    int main()
    {
        ll n,u,v,ans=0,maxx,i;
        double k;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            cin>>w[i];
        }
        for(i=1;i<=n;i++)
        {
            cin>>u>>v;
            u++;
            v++;
            add(u,v);
            add(v,u);
        }
        cin>>k;
        for(i=1;i<=n;i++)
        {
            if(dfn[i]==0)
            {
                tarjan(i,0);
            }
        }
        rt=scc[id][0];
        dfs(rt,scc[id][1]);
        maxx=max(f[rt][0],f[rt][1]);
        rt=scc[id][1];
        dfs(rt,scc[id][0]);
        ans+=max(maxx,f[rt][0]);
        printf("%.1lf\n",1.0*ans*k);
        return 0;
    }
    

luogu P1543 [POI2004] SZP

  • 多倍经验: BZOJ3037 创世纪

  • \(a_{i}\)\(i\) 连一条有向边,这样就形成了基环外向树森林。

  • \(f_{x,0/1}\) 表示 \(x\) 不选/选时,以 \(x\) 为根的子树的最多选择个数,状态转移方程为 \(\begin{cases} f_{x,0}=\sum\limits_{y \in Son(x)} \max(f_{y,0},f_{y,1}) \\ f_{x,1}=1+\max\limits_{y \in Son(x)} \{ f_{y,0}+\sum\limits_{z \in Son(x),z \ne y} \max(f_{z,0},f_{z,1}) \}=1+f_{x,0}- \min\limits_{y \in Son(x)} \{ \max(f_{y,0},f_{y,1})-f_{y,0} \} \end{cases}\)

    点击查看代码
    struct node
    {
        int nxt,to;
    }e[2000010];
    int head[2000010],vis[2000010],u[2000010],v[2000010],f[2000010][2],rt,cnt=0;
    void add(int u,int v)
    {
        cnt++;
        e[cnt].nxt=head[u];
        e[cnt].to=v;
        head[u]=cnt;
    }
    int dfs_huan(int x)
    {
        vis[x]=1;
        return (vis[u[x]]==1)?x:dfs_huan(u[x]);
    }
    void dfs(int x)
    {
        int minn=0x3f3f3f3f;
        vis[x]=1;
        f[x][0]=0;
        for(int i=head[x];i!=0;i=e[i].nxt)
        {
            if(e[i].to==rt)
            {
                f[e[i].to][1]=-0x3f3f3f3f;
            }
            else
            {
                dfs(e[i].to);
                f[x][0]+=max(f[e[i].to][0],f[e[i].to][1]);
                minn=min(minn,max(f[e[i].to][0],f[e[i].to][1])-f[e[i].to][0]);
            }
        }
        f[x][1]=1+f[x][0]-minn;
    }
    int main()
    {
        int n,ans=0,maxx,i;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            v[i]=i;
            cin>>u[i];
            add(u[i],v[i]);
        }
        for(i=1;i<=n;i++)
        {
            if(vis[i]==0)
            {
                rt=dfs_huan(i);
                dfs(rt);
                maxx=max(f[rt][0],f[rt][1]);
                rt=u[rt];
                dfs(rt);
                ans+=max(maxx,f[rt][1]);
            }
        }
        cout<<ans<<endl;
        return 0;
    }
    

luogu P5022 [NOIP2018 提高组] 旅行

  • \(m=n-1\) 时,贪心选取子节点中编号最小优先遍历即可。

  • \(m=n\) 时,暴力枚举删的是哪条边,使其变成一棵树,然后当成 \(m=n-1\) 的情况来做。

    点击查看代码
    int vis[10010],u[10010],v[10010],brokeu,brokev;
    vector<int>e[10010],ans,ls;
    bool check(int u,int v)
    {
        return ((u==brokeu&&v==brokev)||(u==brokev&&v==brokeu))?false:true;
    }
    bool cmp()
    {
        for(int i=0;i<ans.size();i++)
        {
            if(ans[i]>ls[i])
            {
                return true;
            }
            if(ans[i]<ls[i])
            {
                return false;
            }
        }
        return false;
    }
    void dfs1(int x,int fa)
    {
        ans.push_back(x);
        for(int i=0;i<e[x].size();i++)
        {
            if(e[x][i]!=fa)
            {
                dfs1(e[x][i],x);
            }
        }
    }
    void dfs2(int x,int fa)
    {
        vis[x]=1;
        ls.push_back(x);
        for(int i=0;i<e[x].size();i++)
        {
            if(vis[e[x][i]]==0&&check(x,e[x][i])==true)
            {
                dfs2(e[x][i],x);
            }
        }
    }
    int main()
    {
        int n,m,i;
        cin>>n>>m;
        for(i=1;i<=m;i++)
        {
            cin>>u[i]>>v[i];
            e[u[i]].push_back(v[i]);
            e[v[i]].push_back(u[i]);
        }
        for(i=1;i<=n;i++)
        {
            sort(e[i].begin(),e[i].end());
        }
        if(m==n-1)
        {
            dfs1(1,0);
        }
        else
        {
            for(i=1;i<=m;i++)
            {
                brokeu=u[i];
                brokev=v[i];
                memset(vis,0,sizeof(vis));
                ls.clear();
                dfs2(1,0);
                if(ls.size()==n)
                {
                    if(ans.size()==0||cmp()==true)
                    {
                        ans=ls;
                    }
                }
            }
        }
        for(i=0;i<ans.size();i++)
        {
            cout<<ans[i]<<" ";
        }
        return 0;
    }
    

luogu P4381 [IOI2008] Island

  • 发现无向图不是很好处理,遂当成有向图来处理。

  • 基环树的最长链来自两种情况。

    • 在去掉环上所有边后的某棵子树中。
    • 经过环,其两端分别在在去掉环上所有边后的两棵不同子树中。
  • \(f_{x}\) 表示在去掉环上所有边后的以 \(x\) 为根的子树中,以 \(x\) 为起点的最长链长度; \(g_{x}\) 表示在去掉环上所有边后的以 \(x\) 为根的子树中的最长链长度; \(dis_{x,y}\) 表示环上 \(x,y\) 之间的最短距离。

  • 对于环上任意两点 \(s_{i},s_{j}(s_{i} \ne s_{j})\) ,对答案的贡献为 \(\max(g_{s_{i}},g_{s_{j}},f_{s_{i}}+f_{s_{j}}+dis_{s_{i},s_{j}})\)

  • \(f_{s_{i}}+f_{s_{j}}+dis_{s_{i},s_{j}}\) 类似 AcWing 289. 环路运输 一样处理即可。

    点击查看代码
    struct node
    {
        ll nxt,to,w;
    }e[2000010];
    ll head[2000010],din[2000010],vis[2000010],f[2000010],g[2000010],u[2000010],v[2000010],w[2000010],cnt=0;
    void add(ll u,ll v,ll w)
    {
        cnt++;
        e[cnt].nxt=head[u];
        e[cnt].to=v;
        e[cnt].w=w;
        head[u]=cnt;
    }
    void top_sort(ll n)
    {
        queue<ll>q;
        ll x,i;
        for(i=1;i<=n;i++)
        {
            if(din[i]==0)
            {
                q.push(i);
                vis[i]=1;
            }
        }
        while(q.empty()==0)
        {
            x=q.front();
            q.pop();
            for(i=head[x];i!=0;i=e[i].nxt)
            {
                din[e[i].to]--;
                g[e[i].to]=max(g[e[i].to],max(f[e[i].to]+f[x]+e[i].w,g[x]));
                f[e[i].to]=max(f[e[i].to],f[x]+e[i].w);
                if(din[e[i].to]==0)
                {
                    q.push(e[i].to);
                    vis[e[i].to]=1;
                }
            }
        }
    }
    ll ask(ll y)
    {
        ll x=y,ans1=g[x],ans2=-0x3f3f3f3f,len=w[x],maxx1=f[x],maxx2=f[x];
        for(y=v[x];y!=x;y=v[y])
        {
            vis[y]=1;
            ans1=max(ans1,max(g[y],maxx1+f[y]+len));
            ans2=max(ans2,f[y]+maxx2-len);
            maxx1=max(maxx1,f[y]-len);
            maxx2=max(maxx2,f[y]+len);
            len+=w[y];
        }
        return max(ans1,ans2+len);
    }
    int main()
    {
        ll n,ans=0,i;
        cin>>n;
        for(i=1;i<=n;i++)
        {
            u[i]=i;
            cin>>v[i]>>w[i];
            add(u[i],v[i],w[i]);
            din[v[i]]++;
        }
        top_sort(n);
        for(i=1;i<=n;i++)
        {
            if(vis[i]==0)
            {
                vis[i]=1;
                ans+=ask(i);
            }
        }
        cout<<ans<<endl;
        return 0;
    }
    

[ABC357E] Reachability in Functional Graph

luogu P6486 [COCI2010-2011#4] DUGOVI

CF835F Roads in the Kingdom

luogu P6258 [ICPC2019 WF] Hobson's Trains

luogu P3533 [POI2012]RAN-Rendezvous

BZOJ4203 同桌的你

参考资料

《算法竞赛进阶指南》——李煜东

OI-WiKi

基环树学习笔记

[题解] BZOJ4203 同桌的你

posted @ 2024-03-29 17:56  hzoi_Shadow  阅读(65)  评论(0编辑  收藏  举报
扩大
缩小