基环树两点之间距离
基环树两点之间距离(图文详解)
一、问题定义
基环树 = 一个环 + 环上每个点挂一棵树。
给定基环树上两点 u,v,求它们之间的最短路径长度(带权距离)。
基环树上两点路径只有两种可能:
- 两点在同一棵挂树里:路径完全在树上,不绕环。
- 两点在不同挂树里:路径必须经过环。从
u走到它所属的环点ru,从v走到rv,再在环上从ru到rv取较短的那条弧。
蓝色 = 环点,橙色 = 普通树上的点。
u属于环点ru的挂树,v属于环点rv的挂树。
二、需要预处理的信息
2.1 找环
拓扑剥叶:不断删度数为 1 的点,最后剩下的就是环。
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]。
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。
三、查询公式
情况 1:root[u] == root[v](同一挂树)
路径完全在树上,直接求 LCA:
距离 = dep[u] + dep[v] - 2 * dep[lca(u,v)]
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)
从
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 在同一挂树
lca(u,v)=a,dep[a]=3。
距离 = dep[u]+dep[v]-2*dep[a] = 7+5-6 = 6。✔
例 2:u,v 跨挂树
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)。
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23346327

浙公网安备 33010602011771号