基环树找直径
基环树找直径(C++ 图文详解 · 两种跨环做法)
一、基环树的结构
基环树 = 一个环 + 环上每个点挂一棵树。n 个点 n 条边且连通的无向图就是基环树。
蓝色 = 环上的点,绿色 = 挂在环点上的树。去掉任意一条环边就是一棵普通树,但那样会漏掉“绕远路”的直径,所以必须专门处理。
二、直径只有两类
2a. 完全在一棵挂树内部
路径 端点1 → y → x → z → 端点2 整段都在挂树里。
用普通树形 DP 求:
dp[u]:从u往下走的最远距离dia[u]:子树内部直径
转移时保留最长、次长两条向下路径,dia[u]=max(子dia, 最长+次长)。
2b. 跨过环,从一棵挂树到另一棵挂树
路径 挂树A最深点 → A →(环上一段)→ C → 挂树C最深点。
长度 = h[A] + h[C] + 环上A到C的弧长,其中 h[i] 是环点 i 往挂树走的最远距离。
关键:环上两点
i,j之间有两条弧,基环树直径要取较长的那条。
三、找环:拓扑剥叶
过程:把所有度数为 1 的点(灰色)丢进队列,每次弹出一个,把它的邻居度数 -1;如果邻居度数也变成 1 就继续入队。
结果:最后还剩下的(蓝色)就是环。
queue<int>q;
for(int i=1;i<=n;i++)in_ring[i]=1;
for(int i=1;i<=n;i++)if(deg[i]==1)q.push(i);
while(!q.empty())
{
int u=q.front();q.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)q.push(v);
}
}
}
四、挂树内部 DP 的图示
对每个环点 r,把它挂着的非环部分单独跑 DP:
dp[v1]= 从v1往下走到w1或w2的较大值dia[v1]=w1 → v1 → w2这条链的长度(最长 + 次长)dia[u]还要考虑v1子树内或v2子树内的直径- 最后
h[r]=max(dp[child]+w),dia[r]还要考虑最长+次长(两条子树路径跨过r连起来)
用 calc(r) 返回 (h[r], 挂树整体直径) 给外层。
五、跨环:两条弧
把环展开成一条链看更直观:
设:
dist[i]=cyc0顺时针走到cyc[i]的距离L= 环总长 =Σwwww[i]=cyc[i]到cyc[i+1]的边权(ww[k-1]是cyc[k-1]回cyc0)
对 i<j,两条弧:
正向弧长度 = dist[j]-dist[i]
反向弧长度 = L-(dist[j]-dist[i])
跨环候选:
h[i]+h[j]+max(dist[j]-dist[i], L-(dist[j]-dist[i]))
下面两种做法都能在 O(k) 内处理掉这一部分。
六、方法一:前缀和 + 后缀和
max(a,b) 可以拆成两个式子分别求最大再取 max。于是对 i<j:
6a. 正向弧(走 dist[j]-dist[i])
h[i]+h[j]+dist[j]-dist[i]
= (h[i]-dist[i]) + (h[j]+dist[j]) i<j
固定 j 枚举 i<j,只需前缀最大:
pref[i] = max_{t<=i}(h[t]-dist[t])
对每个 j>=1:
ans = max(ans, pref[j-1] + h[j] + dist[j])
从左往右扫,
pref[j-1]就是到j-1为止h[t]-dist[t]的最大值。
6b. 反向弧(走 L-(dist[j]-dist[i]))
h[i]+h[j]+L-dist[j]+dist[i]
= (h[i]+dist[i]) + (h[j]-dist[j]) + L i<j
固定 i 枚举 j>i,只需后缀最大:
suf[i] = max_{t>=i}(h[t]-dist[t])
对每个 i<=k-2:
ans = max(ans, h[i]+dist[i] + suf[i+1] + L)
从右往左扫,
suf[i+1]就是i+1之后所有h[t]-dist[t]的最大值。
6c. 覆盖性
对任意 i<j,简单路径在环上那段只能选一条弧(正向或反向),取大的那条即最优。两个式子分别枚举两种弧,取 max 即得答案。
i=j 属于同一棵挂树内部,已由 dia[cyc[i]] 覆盖。
复杂度 O(k),不需要复制两倍环。
6d. 核心代码
vector<int>dist(k);
dist[0]=0;
for(int i=1;i<k;i++)dist[i]=dist[i-1]+ww[i-1];
int L=dist[k-1]+ww[k-1];
vector<int>pref(k),suf(k);
pref[0]=h[0]-dist[0];
for(int i=1;i<k;i++)pref[i]=max(pref[i-1],h[i]-dist[i]);
suf[k-1]=h[k-1]-dist[k-1];
for(int i=k-2;i>=0;i--)suf[i]=max(suf[i+1],h[i]-dist[i]);
for(int j=1;j<k;j++)
{
ans=max(ans,pref[j-1]+h[j]+dist[j]);
}
for(int i=0;i<k-1;i++)
{
ans=max(ans,h[i]+dist[i]+suf[i+1]+L);
}
七、方法二:单调队列(更常用)
7a. 思路
不拆 max,直接处理:
ans = max_{i<j, j-i<k} ( h[i]+h[j]+dist[j]-dist[i] )
= max_{i<j, j-i<k} ( h[j]+dist[j] + (h[i]-dist[i]) )
固定 j,我们需要窗口 i∈[j-k+1, j-1] 内 h[i]-dist[i] 的最大值。
这正是滑动窗口最大值,用单调队列维护即可。
但环是闭合的,i 和 j 跨越“零点”的情况(比如 i=k-1, j=0)也要算。把环复制成两倍链 [0,2k) 就统一了:
蓝色 = 第一圈,绿色 = 复制出的第二圈,坐标都按
idx%k映射回原环。
7b. 滑动窗口示意
当前扫到
j,窗口是[j-k+1, j-1],单调队列里存h[i%k]-dist[i]递减的下标。
约束j-i<k保证不绕回起点,i<j保证不是同一棵挂树。
7c. 算法流程
队列 dq 存下标,维护 h[i%k]-dist[i] 单调递减
for j in [0, 2k-1]:
① 弹出过期队首:dq.front() < j-k+1
② 若队列非空,用队首更新答案:
ans = max(ans, h[j%k]+dist[j] + h[dq.front()%k]-dist[dq.front()])
③ 把 j 加入队尾,维护递减:
while 队尾值 <= h[j%k]-dist[j]: 弹出队尾
dq.push_back(j)
注意 ② 在 ③ 之前:先算答案再入队,保证
i<j(不和自己配对)。
7d. 核心代码
vector<int>dist(2*k);
dist[0]=0;
for(int i=1;i<2*k;i++)dist[i]=dist[i-1]+ww[(i-1)%k];
deque<int>dq;
for(int j=0;j<2*k;j++)
{
while(!dq.empty()&&dq.front()<j-k+1)dq.pop_front();
if(!dq.empty())
{
int i=dq.front();
ans=max(ans,h[j%k]+dist[j]+h[i%k]-dist[i]);
}
int val=h[j%k]-dist[j];
while(!dq.empty()&&h[dq.back()%k]-dist[dq.back()]<=val)dq.pop_back();
dq.push_back(j);
}
7e. 为什么是 j-k+1
j-i<k ⇔ i>j-k ⇔ 整数 i>=j-k+1。小于 j-k+1 的下标已经绕了整整一圈,会重复经过同一个环点,路径不简单,必须弹出。
j 上界取 2k-1 足够,因为任意有效的 i<j<2k 都能覆盖一对环点。再多加一圈不会更优(j-i<k 已限制)。
八、完整代码
下面给两份都可直接编译运行的版本,只在跨环部分不同。输入格式:n 个点 n 条边,每行 u v w。若边权全为 1,输入 w=1。
版本 A:前缀和 + 后缀和
#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;
int n;
// 邻接表:g[u] = {(v,w), ...},无向边存双向
vector<vector<pair<int,int>>>g(N);
// deg:度数(剥叶用) in_ring:是否仍在环上 vis_ring:环是否已处理
// par:挂树遍历的父节点 dp:子树向下最远距离 dia:子树内部直径
int deg[N],in_ring[N],vis_ring[N],par[N],dp[N],dia[N];
// 处理环点 r 挂着的所有非环子树
// 返回 {h[r], 挂树整体直径}
// h[r] = 从 r 往挂树走的最远距离
// diam = 挂树内部直径(含跨过 r 的两条子树路径)
pair<int,int>calc(int r)
{
int diam=0,best1=0,best2=0; // best1/best2:所有子树对 r 贡献的最长/次长
for(auto [v,w]:g[r])
{
if(in_ring[v])continue; // 跳过环上的邻居
// ---------- 迭代版 BFS 建立以 r 为根的子树顺序 ----------
vector<int>order;
order.push_back(v);
par[v]=r; // 根 v 的父节点是 r
for(int idx=0;idx<(int)order.size();idx++)
{
int u=order[idx];
for(auto [to,ww]:g[u])
{
if(to==par[u]||in_ring[to])continue; // 不走父亲、不进环
par[to]=u;
order.push_back(to);
}
}
// ---------- 逆序做树形 DP ----------
for(int i=(int)order.size()-1;i>=0;i--)
{
int u=order[i];
dp[u]=0;
dia[u]=0;
int b1=0,b2=0; // 子树中向下的最长/次长
for(auto [to,ww]:g[u])
{
if(to==par[u]||in_ring[to])continue;
int c=dp[to]+ww; // 经过该儿子的向下距离
dia[u]=max(dia[u],dia[to]); // 继承儿子的直径
dia[u]=max(dia[u],b1+c); // 之前最长 + 当前儿子,跨 u 成链
if(c>b1) // 维护最长/次长
{
b2=b1;
b1=c;
}else if(c>b2)
{
b2=c;
}
}
dp[u]=b1; // 从 u 往下的最远
dia[u]=max(dia[u],b1+b2); // 两条最长链在 u 汇合
}
// ---------- 把整棵子树 v 汇总到 r 上 ----------
int c=dp[v]+w; // 该子树对 r 的贡献长度
diam=max(diam,dia[v]); // 该子树内部直径
diam=max(diam,best1+c); // 之前最优子树 + 当前子树
if(c>best1) // 维护最长/次长
{
best2=best1;
best1=c;
}else if(c>best2)
{
best2=c;
}
}
diam=max(diam,best1+best2); // 两条最长子树路径跨 r 连起来
return{best1,diam};
}
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>q;
for(int i=1;i<=n;i++)in_ring[i]=1; // 先假设所有点在环上
for(int i=1;i<=n;i++)if(deg[i]==1)q.push(i); // 度数为 1 的先入队
while(!q.empty())
{
int u=q.front();
q.pop();
if(!in_ring[u])continue; // 已处理过
in_ring[u]=0; // 从环上剥掉
for(auto [v,w]:g[u])
{
if(in_ring[v])
{
deg[v]--; // 邻居度数 -1
if(deg[v]==1)q.push(v); // 也变成叶子则入队
}
}
}
// 剩下的 in_ring[i]=1 就是环上的点
int ans=0;
// ---------- 逐个环处理(可能有多棵基环树) ----------
for(int s0=1;s0<=n;s0++)
{
if(in_ring[s0]&&!vis_ring[s0])
{
// 沿环把点、边依次存入 cyc、ww
vector<int>cyc;
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=(int)cyc.size();
// ---------- 计算每个环点的 h[i] 和挂树直径 ----------
vector<int>h(k);
for(int i=0;i<k;i++)
{
auto [hh,dd]=calc(cyc[i]);
h[i]=hh;
ans=max(ans,dd); // 类1:挂树内部直径
}
// ---------- 跨环处理:前缀和 + 后缀和 ----------
// dist[i] = cyc[0] 顺时针到 cyc[i] 的距离
vector<int>dist(k);
dist[0]=0;
for(int i=1;i<k;i++)dist[i]=dist[i-1]+ww[i-1];
int L=dist[k-1]+ww[k-1]; // 环总长
vector<int>pref(k),suf(k);
// pref[i] = max_{t<=i}(h[t]-dist[t])
pref[0]=h[0]-dist[0];
for(int i=1;i<k;i++)pref[i]=max(pref[i-1],h[i]-dist[i]);
// suf[i] = max_{t>=i}(h[t]-dist[t])
suf[k-1]=h[k-1]-dist[k-1];
for(int i=k-2;i>=0;i--)suf[i]=max(suf[i+1],h[i]-dist[i]);
// 类2a:正向弧 (i<j 走 dist[j]-dist[i])
// (h[i]-dist[i]) + (h[j]+dist[j]) 取最大
for(int j=1;j<k;j++)
{
ans=max(ans,pref[j-1]+h[j]+dist[j]);
}
// 类2b:反向弧 (i<j 走 L-(dist[j]-dist[i]))
// (h[i]+dist[i]) + (h[j]-dist[j]) + L 取最大
for(int i=0;i<k-1;i++)
{
ans=max(ans,h[i]+dist[i]+suf[i+1]+L);
}
}
}
cout<<ans<<"\n";
return 0;
}
版本 B:单调队列
和 A 相比,只有 signed main() 里跨环那段从“求 L + 前后缀”改成“复制两倍 + 单调队列”,其余完全一样。
#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;
int n;
// 邻接表:g[u] = {(v,w), ...},无向边存双向
vector<vector<pair<int,int>>>g(N);
// deg:度数(剥叶用) in_ring:是否仍在环上 vis_ring:环是否已处理
// par:挂树遍历的父节点 dp:子树向下最远距离 dia:子树内部直径
int deg[N],in_ring[N],vis_ring[N],par[N],dp[N],dia[N];
// 处理环点 r 挂着的所有非环子树
// 返回 {h[r], 挂树整体直径}
// h[r] = 从 r 往挂树走的最远距离
// diam = 挂树内部直径(含跨过 r 的两条子树路径)
pair<int,int>calc(int r)
{
int diam=0,best1=0,best2=0; // best1/best2:所有子树对 r 贡献的最长/次长
for(auto [v,w]:g[r])
{
if(in_ring[v])continue; // 跳过环上的邻居
// ---------- 迭代版 BFS 建立以 r 为根的子树顺序 ----------
vector<int>order;
order.push_back(v);
par[v]=r; // 根 v 的父节点是 r
for(int idx=0;idx<(int)order.size();idx++)
{
int u=order[idx];
for(auto [to,ww]:g[u])
{
if(to==par[u]||in_ring[to])continue; // 不走父亲、不进环
par[to]=u;
order.push_back(to);
}
}
// ---------- 逆序做树形 DP ----------
for(int i=(int)order.size()-1;i>=0;i--)
{
int u=order[i];
dp[u]=0;
dia[u]=0;
int b1=0,b2=0; // 子树中向下的最长/次长
for(auto [to,ww]:g[u])
{
if(to==par[u]||in_ring[to])continue;
int c=dp[to]+ww; // 经过该儿子的向下距离
dia[u]=max(dia[u],dia[to]); // 继承儿子的直径
dia[u]=max(dia[u],b1+c); // 之前最长 + 当前儿子,跨 u 成链
if(c>b1) // 维护最长/次长
{
b2=b1;
b1=c;
}else if(c>b2)
{
b2=c;
}
}
dp[u]=b1; // 从 u 往下的最远
dia[u]=max(dia[u],b1+b2); // 两条最长链在 u 汇合
}
// ---------- 把整棵子树 v 汇总到 r 上 ----------
int c=dp[v]+w; // 该子树对 r 的贡献长度
diam=max(diam,dia[v]); // 该子树内部直径
diam=max(diam,best1+c); // 之前最优子树 + 当前子树
if(c>best1) // 维护最长/次长
{
best2=best1;
best1=c;
}else if(c>best2)
{
best2=c;
}
}
diam=max(diam,best1+best2); // 两条最长子树路径跨 r 连起来
return{best1,diam};
}
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>q;
for(int i=1;i<=n;i++)in_ring[i]=1; // 先假设所有点在环上
for(int i=1;i<=n;i++)if(deg[i]==1)q.push(i); // 度数为 1 的先入队
while(!q.empty())
{
int u=q.front();
q.pop();
if(!in_ring[u])continue; // 已处理过
in_ring[u]=0; // 从环上剥掉
for(auto [v,w]:g[u])
{
if(in_ring[v])
{
deg[v]--; // 邻居度数 -1
if(deg[v]==1)q.push(v); // 也变成叶子则入队
}
}
}
// 剩下的 in_ring[i]=1 就是环上的点
int ans=0;
// ---------- 逐个环处理(可能有多棵基环树) ----------
for(int s0=1;s0<=n;s0++)
{
if(in_ring[s0]&&!vis_ring[s0])
{
// 沿环把点、边依次存入 cyc、ww
vector<int>cyc;
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=(int)cyc.size();
// ---------- 计算每个环点的 h[i] 和挂树直径 ----------
vector<int>h(k);
for(int i=0;i<k;i++)
{
auto [hh,dd]=calc(cyc[i]);
h[i]=hh;
ans=max(ans,dd); // 类1:挂树内部直径
}
// ---------- 跨环处理:前缀和 + 后缀和 ----------
// dist[i] = cyc[0] 顺时针到 cyc[i] 的距离
vector<int>dist(k);
dist[0]=0;
for(int i=1;i<k;i++)dist[i]=dist[i-1]+ww[i-1];
int L=dist[k-1]+ww[k-1]; // 环总长
vector<int>pref(k),suf(k);
// pref[i] = max_{t<=i}(h[t]-dist[t])
pref[0]=h[0]-dist[0];
for(int i=1;i<k;i++)pref[i]=max(pref[i-1],h[i]-dist[i]);
// suf[i] = max_{t>=i}(h[t]-dist[t])
suf[k-1]=h[k-1]-dist[k-1];
for(int i=k-2;i>=0;i--)suf[i]=max(suf[i+1],h[i]-dist[i]);
// 类2a:正向弧 (i<j 走 dist[j]-dist[i])
// (h[i]-dist[i]) + (h[j]+dist[j]) 取最大
for(int j=1;j<k;j++)
{
ans=max(ans,pref[j-1]+h[j]+dist[j]);
}
// 类2b:反向弧 (i<j 走 L-(dist[j]-dist[i]))
// (h[i]+dist[i]) + (h[j]-dist[j]) + L 取最大
for(int i=0;i<k-1;i++)
{
ans=max(ans,h[i]+dist[i]+suf[i+1]+L);
}
}
}
cout<<ans<<"\n";
return 0;
}
九、验证(k=2)
两条环边 ww[0], ww[1] 都连接 0、1,dist[1]=ww[0],L=ww[0]+ww[1]。
版本 A:
- 正向:
pref[0]+h[1]+dist[1] = (h[0]-0)+h[1]+ww[0] = h[0]+h[1]+ww[0] - 反向:
h[0]+0+suf[1]+L = h[0]+(h[1]-ww[0])+(ww[0]+ww[1]) = h[0]+h[1]+ww[1]
版本 B(j 扫一遍):
j=1:窗口[0,0],取i=0→h[1]+ww[0]+h[0]j=2:窗口[1,1],取i=1→h[0]+(ww[0]+ww[1])+h[1]-ww[0] = h[0]+h[1]+ww[1]j=3、j=4是重复情形,不更优
两种方法都得 h[0]+h[1]+max(ww[0],ww[1]),恰好是两条平行边中较长那条连接两棵挂树的最远路径。✔
总复杂度 O(n)(n 点 n 边,线性遍历 + DP)。
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23345196

浙公网安备 33010602011771号