AIGC标识 基环树两点之间距离

基环树两点之间距离(图文详解)

一、问题定义

基环树 = 一个环 + 环上每个点挂一棵树。
给定基环树上两点 u,v,求它们之间的最短路径长度(带权距离)。

基环树上两点路径只有两种可能:

  1. 两点在同一棵挂树里:路径完全在树上,不绕环。
  2. 两点在不同挂树里:路径必须经过环。从 u 走到它所属的环点 ru,从 v 走到 rv,再在环上从 ru 到 rv 取较短的那条弧。
graph TD A((ru)) --- B((rv)) B --- C((C)) C --- D((D)) D --- A A --- U((u)) U --- U1((u的子节点)) B --- V((v)) V --- V1((v的子节点)) style A fill:#9cf style B fill:#9cf style C fill:#9cf style D fill:#9cf style U fill:#f96 style V fill:#f96

蓝色 = 环点,橙色 = 普通树上的点。u 属于环点 ru 的挂树,v 属于环点 rv 的挂树。


二、需要预处理的信息

2.1 找环

拓扑剥叶:不断删度数为 1 的点,最后剩下的就是环。

graph TD A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A --- L1((叶)) B --- L2((叶)) C --- L3((叶)) D --- L4((叶)) style L1 fill:#ddd style L2 fill:#ddd style L3 fill:#ddd style L4 fill:#ddd style A fill:#9cf style B fill:#9cf style C fill:#9cf style D fill:#9cf

2.2 环上编号与前缀和

沿环走一圈,得到 cyc[0..k-1] 和边权 ww[i](cyc[i] 到 cyc[i+1])。

  • pos[x]:环点 x 在 cyc 中的下标。
  • dist_ring[i]:从 cyc[0] 顺时针到 cyc[i] 的距离,dist_ring[0]=0。
  • L:环总长 = dist_ring[k-1] + ww[k-1]。
graph LR C0((cyc0)) -->|ww0| C1((cyc1)) -->|ww1| C2((cyc2)) -->|...| Ck((cyc k-1)) style C0 fill:#9cf style C1 fill:#9cf style C2 fill:#9cf style Ck fill:#9cf

2.3 每棵挂树的根、距离、层级

从每个环点出发,BFS 它挂着的非环部分(不进入环)。对每个点 x 记录:

数组 含义
root[x] x 所属的环点。若 x 本身是环点,root[x]=x
dep[x] x 到 root[x] 的带权距离
depth[x] x 在挂树中的层级(边数),用于 LCA
up[x][j] x 的 2^j 级祖先,用于 LCA

对于环点 r:root[r]=r, dep[r]=0, depth[r]=0, up[r][0]=0。

graph TD R((环点r<br>dep=0,depth=0)) --- U((u<br>dep=w1,depth=1)) U --- V1((v1<br>dep=w1+w2,depth=2)) U --- V2((v2<br>dep=w1+w3,depth=2)) style R fill:#9cf style U fill:#ffe082 style V1 fill:#c5e1a5 style V2 fill:#c5e1a5

三、查询公式

情况 1:root[u] == root[v](同一挂树)

路径完全在树上,直接求 LCA:

距离 = dep[u] + dep[v] - 2 * dep[lca(u,v)]
graph TD R((root)) --- A((a)) A --- U((u)) A --- B((b)) B --- V((v)) style R fill:#9cf style U fill:#f96 style V fill:#f96 style A fill:#ffe082

u 和 v 的 LCA 是 a。距离 = (dep[u]-dep[a]) + (dep[v]-dep[a])。

注意:如果 u 或 v 是环点,LCA 可能就是环点,公式依然成立。

情况 2:root[u] != root[v](跨挂树)

路径必须经过环:

距离 = dep[u] + dep[v] + 环上 ru 到 rv 的最短弧长

环上两点 ru, rv 的下标为 i=pos[ru], j=pos[rv],两条弧长:

弧1 = abs(dist_ring[i] - dist_ring[j])
弧2 = L - 弧1
最短弧长 = min(弧1, 弧2)
graph TD A((ru)) --- B((...)) B --- C((rv)) C --- D((...)) D --- A style A fill:#9cf style C fill:#9cf style B fill:#ddd style D fill:#ddd

从 ru 到 rv 顺时针走一段,逆时针走另一段,取较短者。


四、复杂度

  • 预处理:O(n log n)(建 LCA 倍增表)。
  • 单次查询:O(log n)(LCA)。

五、完整代码(带注释)

输入格式:
第一行 n(点数 = 边数)。
接下来 n 行 u v w。
然后一行 q,接下来 q 行查询 u v。
输出每个查询的答案。

#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
const int LOG=20;                     // 2^18 > 2e5, 取20足够
int n,q;
vector<vector<pair<int,int>>>g(N);    // 邻接表, g[u]={(v,w)}
int deg[N],in_ring[N],vis_ring[N];     // 剥叶用
int root[N],dep[N],depth[N];           // 挂树信息
int up[N][LOG];                        // 倍增祖先
int pos[N];                            // 环点在cyc中的下标
vector<int>cyc;                        // 环上的点
vector<int>dist_ring;                  // 环上前缀距离
int L;                                 // 环总长

// 标准LCA(倍增), 只在同一挂树内调用
int lca(int u,int v)
{
    if(depth[u]<depth[v])swap(u,v);
    int diff=depth[u]-depth[v];
    for(int j=0;j<LOG;j++)
        if(diff>>j&1)u=up[u][j];
    if(u==v)return u;
    for(int j=LOG-1;j>=0;j--)
    {
        if(up[u][j]!=up[v][j])
        {
            u=up[u][j];
            v=up[v][j];
        }
    }
    return up[u][0];
}

// 查询两点距离
int query(int u,int v)
{
    int ru=root[u],rv=root[v];
    if(ru==rv)                                    // 同一挂树: 树上路径
    {
        int l=lca(u,v);
        return dep[u]+dep[v]-2*dep[l];
    }
    // 跨挂树: u到ru + 环上最短弧 + rv到v
    int i=pos[ru],j=pos[rv];
    int d1=abs(dist_ring[i]-dist_ring[j]);        // 一条弧
    int d2=L-d1;                                  // 另一条弧
    return dep[u]+dep[v]+min(d1,d2);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    // ---------- 读入 ----------
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        g[u].push_back({v,w});
        g[v].push_back({u,w});
        deg[u]++;
        deg[v]++;
    }

    // ---------- 拓扑剥叶找环 ----------
    queue<int>qq;
    for(int i=1;i<=n;i++)in_ring[i]=1;
    for(int i=1;i<=n;i++)if(deg[i]==1)qq.push(i);
    while(!qq.empty())
    {
        int u=qq.front();
        qq.pop();
        if(!in_ring[u])continue;
        in_ring[u]=0;
        for(auto [v,w]:g[u])
        {
            if(in_ring[v])
            {
                deg[v]--;
                if(deg[v]==1)qq.push(v);
            }
        }
    }

    // ---------- 沿环收集点与边 ----------
    int s0=-1;
    for(int i=1;i<=n;i++)
    {
        if(in_ring[i])
        {
            s0=i;
            break;
        }
    }
    vector<int>ww;                                // ww[i] = cyc[i]->cyc[i+1]
    int cur=s0,pre=0;
    do
    {
        cyc.push_back(cur);
        vis_ring[cur]=1;
        int nxt=0,nw=0;
        for(auto [v,w]:g[cur])
        {
            if(in_ring[v]&&v!=pre)
            {
                nxt=v;
                nw=w;
                break;
            }
        }
        ww.push_back(nw);
        pre=cur;
        cur=nxt;
    }while(cur!=s0);
    int k=cyc.size();
    for(int i=0;i<k;i++)pos[cyc[i]]=i;
    dist_ring.resize(k);
    dist_ring[0]=0;
    for(int i=1;i<k;i++)dist_ring[i]=dist_ring[i-1]+ww[i-1];
    L=dist_ring[k-1]+ww[k-1];

    // ---------- 初始化挂树信息 ----------
    for(int i=0;i<=n;i++)
    {
        root[i]=-1;
        dep[i]=0;
        depth[i]=0;
        for(int j=0;j<LOG;j++)up[i][j]=0;
    }
    depth[0]=-1;                                  // 虚拟0号点深度-1
    for(int r:cyc)                                // 环点初始化
    {
        root[r]=r;
        dep[r]=0;
        depth[r]=0;
        up[r][0]=0;
    }

    // ---------- BFS每棵挂树 ----------
    for(int r:cyc)
    {
        queue<int>que;
        que.push(r);
        while(!que.empty())
        {
            int u=que.front();
            que.pop();
            for(auto [v,w]:g[u])
            {
                if(in_ring[v])continue;           // 不进入环
                if(root[v]!=-1)continue;          // 已访问
                root[v]=r;
                dep[v]=dep[u]+w;
                depth[v]=depth[u]+1;
                up[v][0]=u;
                que.push(v);
            }
        }
    }

    // ---------- 建立倍增表 ----------
    for(int j=1;j<LOG;j++)
        for(int i=1;i<=n;i++)
            up[i][j]=up[up[i][j-1]][j-1];

    // ---------- 回答查询 ----------
    cin>>q;
    while(q--)
    {
        int u,v;
        cin>>u>>v;
        cout<<query(u,v)<<"\n";
    }
    return 0;
}

六、小验证

例 1:u,v 在同一挂树

graph TD R((环点r<br>dep=0)) --- A((a<br>dep=3)) A --- U((u<br>dep=7)) A --- V((v<br>dep=5)) style R fill:#9cf style U fill:#f96 style V fill:#f96 style A fill:#ffe082

lca(u,v)=a,dep[a]=3。
距离 = dep[u]+dep[v]-2*dep[a] = 7+5-6 = 6。✔

例 2:u,v 跨挂树

graph TD A((ru<br>pos=0)) --- B((...)) --- C((rv<br>pos=3)) C --- D((...)) --- A A --- U((u<br>dep=2)) C --- V((v<br>dep=4)) style A fill:#9cf style C fill:#9cf style U fill:#f96 style V fill:#f96

dist_ring[0]=0,dist_ring[3]=5,L=12。
弧1 = |0-5|=5,弧2 = 12-5=7,最短 = 5。
距离 = 2+4+5 = 11。✔

例 3:u,v 都是环点

dep[u]=dep[v]=0,距离 = 环上最短弧长。✔


七、要点总结

情况 条件 公式
同一挂树 root[u] == root[v] dep[u]+dep[v]-2*dep[lca]
跨挂树 root[u] != root[v] dep[u]+dep[v]+min(弧1,弧2)
  • dep 是带权距离,depth 是层级(LCA 用)。
  • 环上两条弧:d1=abs(dist_ring[i]-dist_ring[j]),d2=L-d1。
  • 预处理 O(n log n),查询 O(log n)。
posted @ 2026-10-18 13:28  Aries2011  阅读(11)  评论(0)    收藏  举报