树的直径
树的直径是树上最长的路径。
我们可以用树形 dp 或者双 dfs(换根 dp) 求解直径的长度以及两个端点。
主要使用双 dfs。先以 1 1 1 为根找到距离最远点 d 1 d1 d1, d 1 d1 d1 是直径的其中一端点,然后以 d 1 d1 d1 为根找最长距离 D D D 以及最远点 d 2 d2 d2, d 2 d2 d2 是另一端点,而 D D D 就是树的直径。
做法正确性证明见 OI-WIKI。
我的题单。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9;
ll n;
ll idx,head[N];
struct edge
{
ll to,next,w;
}e[N<<1];
void addedge(ll u,ll v,ll w)
{
idx++;
e[idx].to=v;
e[idx].next=head[u];
e[idx].w=w;
head[u]=idx;
}
ll dis[N],d1,d2,D;
void dfs1(ll u,ll fa)
{
if(dis[u]>dis[d1])d1=u;
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs1(v,u);
}
}
void dfs2(ll u,ll fa)
{
if(dis[u]>dis[d2])
{
d2=u;
D=dis[u];
}
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs2(v,u);
}
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<n;i++)
{
ll u,v;
scanf("%lld%lld",&u,&v);
addedge(u,v,1);
addedge(v,u,1);
}
dfs1(1,0);
dis[d1]=0;
dfs2(d1,0);
printf("%lld\n",D);
return 0;
}
1.洛谷 P3304 SDOI2013 直径
题意
对于一棵 n n n 个节点的树,求其直径的长度,以及有多少条边满足所有的直径都经过该边。
2 ≤ n ≤ 200000 2\le n\le 200000 2≤n≤200000,所有点的编号都在 1 ∼ N 1\sim N 1∼N 的范围内,边的权值 ≤ 1 0 9 \le10^9 ≤109。
思路
先标记出其中一条直径;显然,符合条件的边肯定在这个直径上,并且这些边是连续的。
以第二端点 d 2 d2 d2 为根,求出每个节点的 d i s dis dis;然后从 d 2 d2 d2 开始枚举直径上的点 i i i,计算其不经过直径上的点能到达的最长长度 n d nd nd(即走末端)。
如果
n
d
=
d
i
s
i
nd=dis_i
nd=disi,说明存在经过
i
i
i 的、从
d
1
d1
d1 开始的另一段直径,将该点记作
r
r
r。

如图,比如枚举到
2
2
2,
n
d
=
d
2
,
9
=
2
=
d
i
s
2
nd=d_{2,9}=2=dis_2
nd=d2,9=2=dis2,那么从
2
2
2 开始直径开始重叠。
同理如果 n d = D − d i s i nd=D-dis_i nd=D−disi,说明存在经过 i i i 的、从 d 2 d2 d2 开始的另一段直径,即直径开始分岔,将该点记作 l l l。
答案就是 d l , r d_{l,r} dl,r,其实可以把直径上的点按照以 d 2 d2 d2 为根遍历的顺序记录,下标相减即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9;
ll n;
ll idx,head[N];
struct edge
{
ll to,next,w;
}e[N<<1];
void addedge(ll u,ll v,ll w)
{
idx++;
e[idx].to=v;
e[idx].next=head[u];
e[idx].w=w;
head[u]=idx;
}
ll dis[N],d1,d2,D,fat[N];
void dfs1(ll u,ll fa)
{
if(dis[u]>dis[d1])d1=u;
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs1(v,u);
}
}
void dfs2(ll u,ll fa)
{
fat[u]=fa;
if(dis[u]>dis[d2])
{
d2=u;
D=dis[u];
}
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs2(v,u);
}
}
bool vis[N];
ll tot,b[N];
void getD(ll x)
{
while(x)
{
b[++tot]=x;
vis[x]=1;
x=fat[x];
}
}
ll diss[N],nd;
void dp(ll u,ll fa)
{
if(diss[u]>nd)nd=diss[u];
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa||vis[v])continue;
diss[v]=diss[u]+w;
dp(v,u);
}
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<n;i++)
{
ll u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
addedge(u,v,w);
addedge(v,u,w);
}
dfs1(1,0);
dis[d1]=0;
dfs2(d1,0);
printf("%lld\n",D);
getD(d2);
ll r=tot,l=1;
for(int i=tot;i>=1;i--)
{
nd=0;
dp(b[i],0);
if(nd==dis[b[i]])r=i;//直径重叠开始
if(nd==D-dis[b[i]])//直径将要分岔
{
l=i;
break;
}
}
printf("%lld",r-l);
return 0;
}
2.洛谷 P5536 核心城市
题意
(SMOJ 题面)
某落后的国家有 n n n 个村庄, n − 1 n-1 n−1 条长度为 1 1 1 公里的道路,每条道路连接两个村庄,村庄之间可以通过这 n − 1 n-1 n−1 条道路相互到达,现在给了改造村民的生活水平,准备在这 n n n 个村庄建一些发电站,这样冬天就可以供暖。但是为了节约成本,只能从 n n n 个村庄里面挑选出 k k k 个作为建设基地,同时为了节约维护成本,这 k k k 个被选择建设供电站的村庄必须不通过其他村庄的情况下可以两两相互到达,并且其余未被选中的村庄需要离含有供电站的村庄的距离最大值尽量小,求出这个最小值。定义未被选中的村庄到达含有供电站的村庄的距离为该未被选中的村庄到达 k k k 个有供电站的村庄的距离的最小值。
1 ≤ k < n ≤ 1 0 5 1 \le k < n \le 10 ^ 5 1≤k<n≤105。
思路
直径的中点 M i d Mid Mid 到其余所有点距离的最大值,是整棵树最小的,因为总要有最长距离通往直径其中一端点。
因此我们让 M i d Mid Mid 作为其中一个核心城市,然后再其周围“bfs”(尽量填 M i d Mid Mid 周围的):我们计算以 M i d Mid Mid 为根,每个点 i i i 向下走的最远距离 m l e n i mlen_i mleni。因为要求距离尽量小,因此我们取 m l e n mlen mlen 第 k + 1 k+1 k+1 大的城市,其再往上走 1 1 1 就是另外的核心城市,前面的 k k k 座城市都是核心城市。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9;
ll n,k;
ll idx,head[N];
struct edge
{
ll to,next,w;
}e[N<<1];
void addedge(ll u,ll v,ll w)
{
idx++;
e[idx].to=v;
e[idx].next=head[u];
e[idx].w=w;
head[u]=idx;
}
ll dis[N],d1,d2,D,fat[N];
void dfs1(ll u,ll fa)
{
if(dis[u]>dis[d1])d1=u;
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs1(v,u);
}
}
void dfs2(ll u,ll fa)
{
fat[u]=fa;
if(dis[u]>dis[d2])
{
d2=u;
D=dis[u];
}
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs2(v,u);
}
}
ll dep[N],mdep[N],mlen[N];//深度 可到达最深深度 距离最深点距离
//贪心地提起直径中点Mid为根,将周围的点变成核心城市
void dp(ll u,ll fa)
{
mdep[u]=dep[u];
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dep[v]=dep[u]+w;
dp(v,u);
mdep[u]=max(mdep[u],mdep[v]);
}
}
bool cmp(ll x,ll y)
{
return x>y;
}
int main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<n;i++)
{
ll u,v;
scanf("%lld%lld",&u,&v);
addedge(u,v,1);
addedge(v,u,1);
}
dfs1(1,0);
dis[d1]=0;
dfs2(d1,0);
ll Mid=d2;
for(int i=1;i<=(D+1)/2;i++)//直径中点距离短
Mid=fat[Mid];
dep[Mid]=1;
dp(Mid,0);
for(int i=1;i<=n;i++)
mlen[i]=mdep[i]-dep[i];
sort(mlen+1,mlen+n+1,cmp);
printf("%lld",mlen[k+1]+1);//往上一个就是核心城市
return 0;
}
3.洛谷 P6118 JOI2019 独特的城市
我的博客。
4.洛谷 P3629 APIO2010 巡逻
题意
在一个地区中有 n n n 个村庄,编号为 1 , 2 , … , n 1, 2, \dots, n 1,2,…,n。有 n − 1 n-1 n−1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任何一个村庄。每条道路的长度均为 1 1 1 个单位。为保证该地区的安全,巡警车每天要到所有的道路上巡逻。警察局设在编号为 1 1 1 的村庄里,每天巡警车总是从警察局出发,最终又回到警察局。下图表示一个有 8 8 8 个村庄的地区,其中村庄用圆表示(其中村庄 1 1 1 用黑色的圆表示),道路是连接这些圆的线段。为了遍历所有的道路,巡警车需要走的距离为 14 14 14 个单位,每条道路都需要经过两次。

为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K K K 条新的道路,每条新道路可以连接任意两个村庄。两条新道路可以在同一个村庄会合或结束,如下面的图例 ©。一条新道路甚至可以是一个环,即其两端连接到同一个村庄。由于资金有限, K K K 只能是 1 1 1 或 2 2 2。同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。下图给出了一些建立新道路的例子:

在 (a) 中,新建了一条道路,总的距离是 11 11 11。在 (b) 中,新建了两条道路,总的巡逻距离是 10 10 10。在 © 中,新建了两条道路,但由于巡警车要经过每条新道路正好一次,总的距离变为了 15 15 15。试编写一个程序,读取村庄间道路的信息和需要新建的道路数,计算出最佳的新建道路的方案使得总的巡逻距离最小,并输出这个最小的巡逻距离。
3 ≤ n ≤ 1 0 5 3≤n≤10^5 3≤n≤105, k ∈ { 1 , 2 } k\in\{1,2\} k∈{1,2}。
思路
k k k 只有两种情况,我们可以分类讨论:
k = 1 k=1 k=1
我们可以加一条捷径跳过直径,因为直径是整棵树中路径最长的,答案就是 2 ( n − 1 ) − D + 1 2(n-1)-D+1 2(n−1)−D+1。
k = 2 k=2 k=2
一条用来跳过直径,我们再找一条和直径不重叠的最长路径 D 2 D2 D2。我们只需要将直径边权删去(置无限小)维护每个点的 m d i s u mdis_u mdisu 表示树上 u u u 出发可以走的最远距离。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9,inf=3e14;
ll n,k;
ll idx=1,head[N];
struct edge
{
ll to,next,w;
}e[N<<1];
void addedge(ll u,ll v,ll w)
{
idx++;
e[idx].to=v;
e[idx].next=head[u];
e[idx].w=w;
head[u]=idx;
}
ll dis[N],d1,d2,D,fat[N];
void dfs1(ll u,ll fa)
{
if(dis[u]>dis[d1])d1=u;
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs1(v,u);
}
}
void dfs2(ll u,ll fa)
{
fat[u]=fa;
if(dis[u]>dis[d2])
{
d2=u;
D=dis[u];
}
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs2(v,u);
}
}
bool isD[N];
void getD(ll x)
{
while(x)
{
isD[x]=1;
x=fat[x];
}
}
void delD(ll u,ll fa)
{
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to;
if(v==fa||!isD[v])continue;
e[i].w=e[i^1].w=-1;
delD(v,u);
}
}
ll D2,mdis[N];//i出发走的最远距离
void dp(ll u,ll fa)
{
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dp(v,u);
// cout<<u<<" "<<v<<" "<<w<<endl;
D2=max(D2,mdis[u]+mdis[v]+w);
mdis[u]=max(mdis[u],mdis[v]+w);
}
}
int main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<n;i++)
{
ll u,v;
scanf("%lld%lld",&u,&v);
addedge(u,v,1);
addedge(v,u,1);
}
dfs1(1,0);
dis[d1]=0;
dfs2(d1,0);
if(k==1)
{
printf("%lld",2*(n-1)-D+1);
return 0;
}
getD(d2);
delD(d1,0);
dis[1]=0;
dp(1,0);
// cout<<D<<" "<<D2<<endl;
printf("%lld",2*(n-1)-D+1-D2+1);
return 0;
}
5.洛谷 P4408 NOI2003 逃学的小孩
题意
Chris 居住的城市由 N N N 个居住点和若干条连接居住点的双向街道组成,经过街道 x x x 需花费 T x T_{x} Tx 分钟。可以保证,任意两个居住点间有且仅有一条通路。Chris 家在点 C C C,Shermie 和 Yashiro 分别住在点 A A A 和点 B B B。Chris 的老师和 Chris 的父母都有城市地图,但 Chris 的父母知道点 A A A、 B B B、 C C C 的具体位置而 Chris 的老师不知。
为了尽快找到 Chris,Chris 的父母会遵守以下两条规则:
- 如果 A A A 距离 C C C 比 B B B 距离 C C C 近,那么 Chris 的父母先去 Shermie 家寻找 Chris,如果找不到,Chris 的父母再去 Yashiro 家;反之亦然。
- Chris 的父母总沿着两点间唯一的通路行走。
显然,Chris 的老师知道 Chris 的父母在寻找 Chris 的过程中会遵守以上两条规则,但由于他并不知道 A A A、 B B B、 C C C 的具体位置,所以现在他希望你告诉他,最坏情况下 Chris的父母要耗费多长时间才能找到 Chris?
3 ≤ N ≤ 2 × 1 0 5 3 \le N \le 2\times 10^5 3≤N≤2×105, 1 ≤ U i , V i ≤ N 1 \le U_{i},V_{i} \le N 1≤Ui,Vi≤N, 0 ≤ T i ≤ 1 0 9 0 \le T_{i} \le 10^{9} 0≤Ti≤109。
思路
先转化题意,就是在一棵树上找到 3 3 3 个点 A A A、 B B B、 C C C,使 C A + A B CA+AB CA+AB 最大,同时满足 C B > C A CB>CA CB>CA。
我们让 A B AB AB 成为直径, A A A、 B B B 作为直径两端点之一。然后枚举 C C C, C A CA CA 和 C B CB CB 总有一个是 C C C 在整棵树中的最远距离,另一个是次远距离,取二者最小值再与直径相加,就是从 C C C 出发的最长耗费时间。答案就是所有 C ∈ [ 1 , n ] C\in[1,n] C∈[1,n] 的最大值。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3e5+9;
ll n,m;
ll idx,head[N];
struct edge
{
ll to,next,w;
}e[N<<1];
void addedge(ll u,ll v,ll w)
{
idx++;
e[idx].to=v;
e[idx].next=head[u];
e[idx].w=w;
head[u]=idx;
}
ll dis[N],d1,d2,D;
void dfs1(ll u,ll fa)
{
if(dis[u]>dis[d1])d1=u;
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs1(v,u);
}
}
void dfs2(ll u,ll fa)
{
if(dis[u]>dis[d2])
{
d2=u;
D=dis[u];
}
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs2(v,u);
}
}
ll ds[2][N];
void dfs0(ll u,ll fa,ll op)
{
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
ds[op][v]=ds[op][u]+w;
dfs0(v,u,op);
}
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
ll u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
addedge(u,v,w);
addedge(v,u,w);
}
dfs1(1,0);
dis[d1]=0;
dfs2(d1,0);//AB为两端点
dfs0(d1,0,0);
dfs0(d2,0,1);
ll ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,D+min(ds[0][i],ds[1][i]));
printf("%lld",ans);
return 0;
}
6.洛谷 P8981 距离
7.洛谷 P1099 树网的核
题意
设
T
=
(
V
,
E
,
W
)
T=(V,E,W)
T=(V,E,W) 是一个无圈且连通的无向图(也称为无根树),每条边都有正整数的权,我们称
T
T
T 为树网(treenetwork),其中
V
V
V,
E
E
E 分别表示结点与边的集合,
W
W
W 表示各边长度的集合,并设
T
T
T 有
n
n
n 个结点。
路径:树网中任何两结点
a
a
a,
b
b
b 都存在唯一的一条简单路径,用
d
(
a
,
b
)
d(a, b)
d(a,b) 表示以
a
,
b
a, b
a,b 为端点的路径的长度,它是该路径上各边长度之和。我们称
d
(
a
,
b
)
d(a, b)
d(a,b) 为
a
,
b
a, b
a,b 两结点间的距离。
D ( v , P ) = min { d ( v , u ) } D(v, P)=\min\{d(v, u)\} D(v,P)=min{d(v,u)}, u u u 为路径 P P P 上的结点。
树网的直径:树网中最长的路径称为树网的直径。对于给定的树网 T T T,直径不一定是唯一的,但可以证明:各直径的中点(不一定恰好是某个结点,可能在某条边的内部)是唯一的,我们称该点为树网的中心。
偏心距 E C C ( F ) \mathrm{ECC}(F) ECC(F):树网 T T T 中距路径 F F F 最远的结点到路径 F F F 的距离,即
E C C ( F ) = max { D ( v , F ) , v ∈ V } \mathrm{ECC}(F)=\max\{D(v, F),v \in V\} ECC(F)=max{D(v,F),v∈V}
任务:对于给定的树网
T
=
(
V
,
E
,
W
)
T=(V, E, W)
T=(V,E,W) 和非负整数
s
s
s,求一个路径
F
F
F,他是某直径上的一段路径(该路径两端均为树网中的结点),其长度不超过
s
s
s(可以等于
s
s
s),使偏心距
E
C
C
(
F
)
\mathrm{ECC}(F)
ECC(F) 最小。我们称这个路径为树网
T
=
(
V
,
E
,
W
)
T=(V, E, W)
T=(V,E,W) 的核(Core)。必要时,
F
F
F 可以退化为某个结点。一般来说,在上述定义下,核不一定只有一个,但最小偏心距是唯一的。
下面的图给出了树网的一个实例。图中,
A
−
B
A-B
A−B 与
A
−
C
A-C
A−C 是两条直径,长度均为
20
20
20。点
W
W
W 是树网的中心,
E
F
EF
EF 边的长度为
5
5
5。如果指定
s
=
11
s=11
s=11,则树网的核为路径DEFG(也可以取为路径DEF),偏心距为
8
8
8。如果指定
s
=
0
s=0
s=0(或
s
=
1
s=1
s=1、
s
=
2
s=2
s=2),则树网的核为结点
F
F
F,偏心距为
12
12
12。

2
≤
n
≤
300
2\le n \le 300
2≤n≤300,
0
≤
s
≤
1
0
3
0\le s\le10^3
0≤s≤103,
1
≤
u
,
v
≤
n
1 \leq u, v \leq n
1≤u,v≤n,
0
≤
w
≤
1
0
3
0 \leq w \leq 10^3
0≤w≤103。
思路
当然是先标记直径,然后直径两端分别到直径的距离取小。
最后直接计算未标记的节点到直径的距离即可——当遍历到标记点时退出。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3e5+9,inf=3e14;
ll n,s;
ll idx,head[N];
struct edge
{
ll to,next,w;
}e[N<<1];
void addedge(ll u,ll v,ll w)
{
idx++;
e[idx].to=v;
e[idx].next=head[u];
e[idx].w=w;
head[u]=idx;
}
ll dis[N],d1,d2,D,fat[N];
void dfs1(ll u,ll fa)
{
if(dis[u]>dis[d1])d1=u;
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs1(v,u);
}
}
void dfs2(ll u,ll fa)
{
fat[u]=fa;
if(dis[u]>dis[d2])
{
d2=u;
D=dis[u];
}
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa)continue;
dis[v]=dis[u]+w;
dfs2(v,u);
}
}
bool isD[N];
void getD(ll x)
{
while(x)
{
isD[x]=1;
x=fat[x];
}
}
void dp(ll u,ll fa)
{
for(int i=head[u];i;i=e[i].next)
{
ll v=e[i].to,w=e[i].w;
if(v==fa||isD[v])continue;
dis[v]=dis[u]+w;
dp(v,u);
}
}
int main()
{
scanf("%lld%lld",&n,&s);
for(int i=1;i<n;i++)
{
ll u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
addedge(u,v,w);
addedge(v,u,w);
}
dfs1(1,0);
dis[d1]=0;
dfs2(d1,0);
getD(d2);
ll ecc=inf;
for(int i=d2,j=d2;i;i=fat[i])//i在上j在下
{
while(dis[j]-dis[i]>s)j=fat[j];
ecc=min(ecc,max(dis[i],dis[d2]-dis[j]));
//特定的核,直径两端(更大)到核的距离
}
ll x=d2;
while(x)
{
dis[x]=0;
dp(x,fat[x]);
x=fat[x];
}
for(int i=1;i<=n;i++)//核为整条直径,算每个点到直径的距离
ecc=max(ecc,dis[i]);
printf("%lld",ecc);
return 0;
}
2575

被折叠的 条评论
为什么被折叠?



