树的直径
树的直径,又称树的最长链,定义为一棵树上最远的两个节点的路径,即树上一条不重复经过某一条边的最长的路径。树的直径也可以代指这条路径的长度。
- 树上任意点能到的最远点,一定是树的直径的某个端点。
求解树的直径有两种方法 时间复杂度都为 O ( n ) O(n) O(n)
- 方法一 两遍搜索:从树上任意点u开始DFS(BFS)遍历图,得到距离u最远的结点v,然后从v点开始DFS遍历图,得到距离v最远的结点w, 则v、w之间的距离就是树的直径。
- 方法二 树形dp:DP:显然最长路的两个端点必然是叶子或者根节点。设f(i)表示到i最远的叶子,g(i)表示到i次远的叶子,则有
f ( i ) = m a x f ( j ) + 1 f(i)=max{\ f(j)}+1 f(i)=max f(j)+1
g ( i ) = s e c o n d m a x f ( j ) + 1 g(i)=second_{max}{\ f(j)}+1 g(i)=secondmax f(j)+1
其中j必须是i的儿子,计算顺序是自底向上。最终答案为
m a x f ( i ) + g ( i ) + 1 max{\ f(i)+g(i)\ }+1 max f(i)+g(i) +1 - dp方法的优化:可以直接用一个 p r e pre pre 记录当前节点u 经过遍历到v之前的子节点 到叶子结点的最大路径,且 当前节点u到当前子节点v 到叶子节点的路径长为 d p [ v ] + e [ i ] . w dp[v]+e[i].w dp[v]+e[i].w ,因此可得一条树的直径为 p r e + d p [ v ] + e [ i ] . w pre+dp[v]+e[i].w pre+dp[v]+e[i].w ,与ans取max,再更新pre使其与 d p [ v ] + e [ i ] . w dp[v]+e[i].w dp[v]+e[i].w 取max。注意两次更新的顺序。
#include<iostream>
#include<cstdio>
using namespace std;
int n,cnt,ans;
const int N=1e4+5;
const int M=2*N;
int fa[N],dp[N],head[N];
struct node{
int to,next,w ;
}e[M];
void add(int u,int v,int w){
e[++cnt].to =v; e[cnt].w =w; e[cnt].next=head[u]; head[u]=cnt;
}
void dfs(int u){
int pre=0; //表示以当前节点u向下走的最大路径 即dp[u]
for(int i=head[u];i;i=e[i].next ){
int v=e[i].to ;
if(v==fa[u]) continue;
fa[v]=u;
dfs(v);
ans=max(ans,pre+dp[v]+e[i].w); //一条经过u和v的树的直径
pre=max(pre,dp[v]+e[i].w); //u经过v及其之前子节点中 到叶子结点的最大路径
}
dp[u]=pre;
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w); add(v,u,w);
}
dfs(1);
printf("%d",ans);
}
树的中心
定义:树上任意一条直径所在的链的中点,当直径为偶数的时候,中心由一个点 u u u 构成。当直径为奇数的时候,中心由两个点构成 ( u , v ) ( u , v ) (u,v)。
性质
- 树的中心是唯一的,有一个且仅有一个中心
- 树上任意一个节点 u u u到另外一个任意节点 v v v的的距离是最大值,那么 v v v为树上直径两个端点的其中一个。
带边权树的中心
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。
树的重心
定义:树上的每一个节点都有一个平衡值,该平衡值的定义为,以该节点为根节点,所有子树中节点数量最大的那个子树中节点数量。树的重心是树上的一个节点,其平衡值是树中节点中最小的那个。
性质
-
以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
-
树上所有的点到树的重心的距离之和是最短的,如果有多个重心,那么总距离相等。
-
插入或删除一个点,树的重心的位置最多移动一个单位。
-
若添加一条边连接2棵树,那么新树的重心一定在原来两棵树的重心的路径上。
求解树的重心,核心框架是dfs
#include<iostream>
#include<cstdio>
using namespace std;
int n,cnt,ans=1e9;
const int M=11000,N=2100;
int st[N],head[N];
struct node {
int to,next;
} e[M];
void add(int u,int v) {
e[++cnt].to=v; e[cnt].next=head[u]; head[u]=cnt;
}
int dfs(int u) {
int size=0; //连通块中点的最大值 即当前根的子树大小与另一连通块大小中 较大的一个
int sum=1;//以u为根的子树大小
st[u]=1;
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to ;
if(!st[v]){
int f=dfs(v); //以v为根的子树大小
size=max(size,f);
sum+=f;
}
}
size=max(size,n-sum);//取以u为根的子树的最大值
ans=min(ans,size); //取最大子树的最小值
return sum;
}
int main() {
scanf("%d",&n);
for(int i=1; i<n; i++) {
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
}
printf("%d",dfs(1));
}
例题:P1395 会议
给定一棵无根树,求多个重心中编号最小的一个,并求出各个点到该重心的距离之和。
在模改的过程中注意各个变量的意义。
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
int n,cnt,ans=1e9,pos,res;
const int M=101000,N=51000;
int st[N],head[N],dep[N];
struct node {
int to,next;
} e[M];
void add(int u,int v) {
e[++cnt].to =v;e[cnt].next =head[u];
head[u]=cnt;
}
int dfs(int u,int fa) {
int size=0; //当前根的子树大小与另一连通块大小中 较大的一个
int sum=1;//以u为根的子树大小
st[u]=1;
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to ;
if(!st[v]&&fa!=v){
int f=dfs(v,u); //以v为根的子树大小
size=max(size,f);
sum+=f;
}
}
size=max(size,n-sum);
if(ans>size){
ans=size, pos=u; //更新重心
}
else if(ans==size) //多个重心选取编号较小的一个
pos=min(u,pos);
return sum;
}
void bfs(int x){
queue<int>q;
q.push(x);
res=0;
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].next ){
int v=e[i].to;
if(!dep[v]&&v!=x){
dep[v]=dep[u]+1;
res+=dep[v];
q.push(v);
}
}
}
}
int main() {
scanf("%d",&n);
for(int i=1; i<n; i++) {
int u,v;
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
dfs(1,0);
bfs(pos);
printf("%d %d",pos,res);
}
带点权树的重心
即树中每个节点带点权,求树的重心
状态表示:
f
[
u
]
f[u]
f[u]表示以u为根的总距离,
s
i
z
e
[
u
]
size[u]
size[u]表示以u为根的子树的大小(结点数乘以权值)。
状态计算:对于每个u能达到的点v,有:
f
[
v
]
=
f
[
u
]
+
s
i
z
e
[
1
]
−
s
i
z
e
[
v
]
−
s
i
z
e
[
v
]
f[v]=f[u]+size[1]-size[v]-size[v]
f[v]=f[u]+size[1]−size[v]−size[v]
画棵树模拟一下就明白了
显然,
a
n
s
=
m
i
n
(
f
[
i
]
,
1
<
=
i
<
=
n
)
ans=min(f[i],1<=i<=n)
ans=min(f[i],1<=i<=n)
任意以一个点为根dfs一遍,求出以该点为根的总距离。方便起见,我们就以1为根。
例题:P1364 医院设置
注意预处理和dp过程两个递归方式的不同,有助于更好的理解递归
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1100;
int n,ans=1e9,cnt;
int f[N],w[N],size[N],head[N];
struct node{
int to,next;
}e[N<<2];
void add(int u,int v){
e[++cnt].to=v; e[cnt].next =head[u]; head[u]=cnt;
}
void dfs(int u,int fa,int dep){
size[u]=w[u]; //初始化带点权
for(int i=head[u];i;i=e[i].next){
int v=e[i].to ;
if(v!=fa){
dfs(v,u,dep+1); //自底向上 先递归到最底层 再累加计算
size[u]+=size[v]; //预处理子树大小 带点权
}
}
f[1]+=w[u]*dep;//预处理 f[1]
}
void dp(int u,int fa){
for(int i=head[u];i;i=e[i].next ){
int v=e[i].to;
if(v!=fa){
f[v]=f[u]+size[1]-size[v]-size[v]; //状态转移
dp(v,u); //由于是u->v 正推 所以先转移状态 再递推
}
}
ans=min(ans,f[u]); //ans
return;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int u,v;
scanf("%d%d%d",&w[i],&u,&v);
if(u!=0) add(u,i),add(i,u);
if(v!=0) add(v,i),add(i,v);
}
dfs(1,0,0);
dp(1,0);
printf("%d",ans);
}