[c++算法] 树的直径,包教包会!

news/2025/2/26 13:10:36/文章来源:https://www.cnblogs.com/doooge/p/18738229

哈喽大家好,我是 doooge。今天我们要将数论中的一个算法-树的直径。

$$\Huge 树的直径 详解$$

1.树的直径是什么

这是一棵图论中的树:

一棵树

这棵树的直径就是这棵树中最长的一条简单路径

2.树的直径怎么求

2.1暴力算法

直接对每个点进行 DFS,找到每个点离最远的点的距离,最后求出最长的一条路,也就是树的直径。

时间复杂度:$O(n^2)$

代码我就只放 DFS 的了,其他的没什么必要:

void dfs(int x,int fa,int sum){dis[x]=sum;for(auto i:v[x]){if(i==fa)continue;dfs(i,x,sum+1);}return;
} 

重点:2.2 DFS直接求

直接说结论:

对于每一个点 $x$,离 $x$ 最远的点一定是树的直径的一个顶点。

为什么呢?

我们可以用反证法来推导:

假设树的直径的端点为 $u$ 和 $v$,设对于每一个离点 $x$ 最远的点 $y$ 不是树的直径的端点 $u$ 和 $v$,按我们可以分类讨论(以下把点 $x$ 到点 $y$ 的路径称作 $x \to y$,它们的距离称作 $dis_{x \to y}$):

  1. 点 $x$ 在树的直径 $u \to v$ 中
  2. 点 $x$ 不在树的直径 $u \to v$ 中,但 $x \to y$ 这条路径与树的直径 $u \to v$ 有一部分重合。
  3. 点 $x$ 不在树的直径 $u \to v$ 中,且 $x \to y$ 这条路径与树的直径 $u \to v$ 完全不重合。

(温馨提示:下面的内容建议自己先推一遍,画棵树想想再看)

先来看情况 $1$,若点 $x$ 在树的直径 $u \to v$ 中且点 $y$ 既不等于 $u$ 也不等于 $v$。

因为 $y$ 既不等于 $u$ 也不等于 $v$,那么 $dis_{x \to y}$ 必定会大于 $dis_{x \to u}$ 和 $dis_{x \to v}$,因为 $dis_{u \to v} = dis_{u \to x} + dis_{x \to v}$,又因为 $dis_{x \to v} < dis_{x \to y}$,那么此时这棵树的直径便是 $u \to y$ 这两条路,与直径的定以不符,所以错误。

再来看情况 $2$,点 $x$ 不在树的直径 $u \to v$ 中,但 $x \to y$ 这条路径与树的直径 $u \to v$ 有一部分重合。这里又可以分成两种情况。

  1. $x \to y$ 被完全包含在 $u \to v$ 内,这是显然不可能的。
  2. $x \to y$ 有一部分包含在 $u \to v$ 内,那我们可以设点 $o$ 为公共部分其中的一个点,那么此时 $dis_{o \to y}$ 一定要大于 $dis_{o \to v}$ 和 $dis_{o \to u}$,与直径的定以不符,所以错误。

最后来看情况 $3$,点 $x$ 不在树的直径 $u \to v$ 中,且 $x \to y$ 这条路径与树的直径 $u \to v$ 完全不重合。

这时,我们设点 $o$ 于 $u \to v$ 内,因为每棵树都是连通的,所以必定有一条 $x \to o$ 路。于是,就得到了一下式子:

$$dis_{u \to v}=dis_{u \to o}+dis_{o \to v}=dis_{u \to o}+dis_{x \to v}-dis_{x \to o}$$

$$dis_{u \to y}=dis_{u \to o}+dis_{o \to y}=dis_{u \to o}+dis_{x \to y}-dis_{x \to o}$$

将两个式子互相抵消,分别得到 $dis_{x \to v}$ 和 $dis_{x \to y}$,因为 $dis_{x \to y} > dis_{x \to v}$,所以得到 $dis_{u \to v} < dis_{u \to y}$,与直径的定以不符,所以错误。

至此,证毕。

于是!我们可以从点 $1$ 开始 DFS,找到离点 $1$ 最远的点 $y$,再进行 DFS 找到离点 $y$ 最远的点,就找到了树的直径。

代码:

#include<bits/stdc++.h>
using namespace std;
int dis,pos;
vector<int>v[100010];
void dfs(int x,int fa,int sum){if(sum>=dis){//注意这里一定是>=而不是>dis=sum;pos=x;}for(auto i:v[x]){if(i==fa)continue;//不能走回头路dfs(i,x,sum+1);}return;
} 
int main(){int n;cin>>n;for(int i=1;i<n;i++){int x,y;cin>>x>>y;v[x].push_back(y);v[y].push_back(x);}dfs(1,-1,0);//找出点ydis=0;//记得清空dis变量dfs(pos,-1,0);cout<<dis<<endl;return 0;
} 

该模版写法不一,也可以用 $dis$ 数组存储距离,DFS 完后再找最大的路径。该带码也同样适用于带边权的树。

时间复杂度:$O(n)$。

注意:该算法只能在所有边权为正数的情况下成立,否则会出问题,具体为什么下面会讲

我们来看这张图:

反例

不难发现,这棵树的直径是 $5 \to 6$ 这一条路,但是如果你从点 $1$ 开始进行 DFS,只能找到点 $3$,因为中间被 $2 \to 4$ 这条边挡住了,从 $1 \to 5$ 不是最优解。

方法3:树形DP

主播主播,你的 DFS 大法确实很强,但是还是太吃条件了,有没有既速度快又没有限制的算法呢?

有的兄弟,有的,像这样的算法,主播还有一个,都是 T0.5 的强势算法,只要你掌握一个,就能秒杀 CSP 树的直径,如果想主播这样两个都会的话,随便进 NOI CSP-S。

好了,回归正题,我们来讲讲树形DP 的写法。$dp_x$ 表示从 $x$ 出发走向 $x$ 的子树中最长的一条路径。

假设有一棵树的根节点为 $root$(我们这里称把 $x$ 的子节点称作为 $x_i$),那么我们的 $dp_{root}$ 就表示从 $root$ 节点出发能走到的最远距离,也就是 $root$ 的子树的最大的深度。所以,我们得要从子树开始更新,也就是在这里:

for(auto i:v[x]){//继续dfsif(i.x==fa)continue;//不能走回头路dfs(i.x);//往下搜索dp[x]=...;//这里开始更新,此时先dfs的子节点会先更新dp
}

那么,我们就可以在遍历子节点 $v_i$ 的时候更行新 $dp_{root}$:

$$dp_{root}=\max(dp_{root},dp_{v_i}+dis_{root \to v_i})$$

其他节点也同理。

这时,有聪明的读者就会说了:你这不是只更新了它的一个子树吗,如果树的直径是这样子,那你的 DP 不是就错了吗?

一开始的那棵树

读者说的没错,我们要考虑图片上的情况。

我们可以设置一个中间节点,比如这张图的中间点就是 $root$ 节点,一条路径可以贯穿一个中间节点的两个子树,而我们的 $dp$ 数组只记录了一个子树的最大的深度,也就是子树的最长路。

于是,我们可以在更新 $dp$ 数组的时候同时更新另一个变量 $ans_x$,表示若 $x$ 为树的直径的中间点,穿过 $x$ 最长的路径的长度。当然,$dp$ 数组也不能落下,但是答案还是存在 $ans$ 数组里。因为要找到两个长度最大的长度,所以更新代码为这样:

$$ans_x=\max(ans_x,dp_x+dp_{v_i}+dis_{x
\to v_i})$$

至于为什么是 $dp_x+dp_{v_i}$ 因为此时的 $dp_x$ 表示的是在 $v_i$ 之前遍历到的子树的最大值,$dp_{v_i}+dis_{x \to v_i}$ 表示这棵子树的最大的长度,所以,$ans$ 数组的更新应该在 $dp$ 数组的更新之前。

代码(我只展示 DFS 部分,剩下的应该不难了吧):

void dfs(int x,int fa){dp[x]=0;for(auto i:v[x]){//'v'是一个结构体vector,里面包含x和w这两个参数if(i.x==fa)continue;//i.x表示遍历到的节点dfs(i.x,x);//继续搜索下去ans[x]=max(ans[x],dp[x]+dp[i.x]+i.w)//i.w表示x到i.x的边权dp[x]=max(dp[x],dp[i.x]+i.w);}
}

3.例题

T1.P8602 [蓝桥杯 2013 省 A] 大臣的旅费

题目描述

很久以前,T 王国空前繁荣。为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。

为节省经费,T 国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。

J 是 T 国重要大臣,他巡查于各大城市之间,体察民情。所以,从一个城市马不停蹄地到另一个城市成了 J 最常做的事情。他有一个钱袋,用于存放往来城市间的路费。

聪明的 J 发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第 $x - 1$ 千米到第 $x$ 千米这一千米中($x$ 是整数),他花费的路费是 $x+10$ 这么多。也就是说走 $1$ 千米花费 $11$,走 $2$ 千米要花费 $23$。

J 大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?

(绝对不是水字数)

思路+代码

这道题乍一看上去确实很乱,但我们可以找找关键句(跟语文课上学的一样)。

如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。 咦?这句话的意思不就是从根节点出发到每一个节点的路径唯一吗?

他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢 咦?这句话不就是要求一棵树上最长的一条路径吗?

综上所述,这道题完完全全就是树的直径的板子,只是读题困难一点而已。需要注意,最后的答案并不是树的直径的长度,而是像题目描述中的这样:

cout<<dis*10+(dis+1)*dis/2<<endl;

OK,这道题就没有其他的坑了,代码如下:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int dis,pos;
struct ll{//个人习惯,见谅int x,w;
};
vector<ll>v[100010];
void dfs(int x,int fa,int sum){if(sum>=dis){dis=sum;pos=x;}for(auto i:v[x]){if(i.x==fa)continue;dfs(i.x,x,sum+i.w);}return;
} 
signed main(){int n;cin>>n;for(int i=1;i<n;i++){int x,y,w;cin>>x>>y>>w;v[x].push_back({y,w});v[y].push_back({x,w});}dfs(1,-1,0);dis=0;dfs(pos,-1,0);cout<<dis*10+(dis+1)*dis/2<<endl;return 0;
} 

难度:$1/5$。

T2.HDU 2196 Computer

请注意,这道题不是洛谷的,需要在 vjudge 上交代码。

题目描述

给定一棵节点为 $N$ 的树($1 \le N \le 10^4$),输出每个节点 $i$ 离 $i$ 最远的节点的长度。

思路+代码

首先,$O(N^2)$ 的暴力 DFS 是不可能的,因为题目中还有 $T$ 组数据。想一想,对于每个节点 $i$ 离 $i$ 最远的点是什么呢?

对的,之前说过,就是树的直径的两个端点!所以离每一个节点 $i$ 最远的点就是树的直径的两端的节点 $u$ 和 $v$。

于是,我们可以用 $O(N)$ 的 DFS 先将树的直径的两个端点求出来,在继续用 $O(N)$ 的 DFS 求出对每个节点的距离,对于节点 $i$,它的答案就是:

$$\max(dis_{u \to i},dis_{v \to i})$$

代码:

#include<bits/stdc++.h>
using namespace std;
int dis[100010],n;
bool f[100010];
struct ll{int x,w;
};
vector<ll>v[100010];
void dfs(int x,int sum){dis[x]=max(dis[x],sum);f[x]=true;for(int i=0;i<v[x].size();i++){ll tmp=v[x][i];if(f[tmp.x])continue;dfs(tmp.x,sum+tmp.w);}return;
}
void solve(){memset(dis,0,sizeof(dis));memset(f,false,sizeof(f));for(int i=1;i<=n;i++){v[i].clear();}for(int i=1;i<n;i++){int x,w;cin>>x>>w;v[i+1].push_back({x,w});v[x].push_back({i+1,w});}dfs(1,0);int mx=-1e9,pos=-1,pos2=-1;for(int i=1;i<=n;i++){pos=(dis[i]>=mx?i:pos);mx=max(mx,dis[i]);}memset(dis,0,sizeof(dis));memset(f,false,sizeof(f));dfs(pos,0);mx=-1e9;for(int i=1;i<=n;i++){pos2=(dis[i]>=mx?i:pos2);mx=max(mx,dis[i]);}memset(f,false,sizeof(f));dfs(pos2,0);for(int i=1;i<=n;i++){cout<<dis[i]<<'\n';}
}
int main(){while(cin>>n){solve();}return 0;
}

难度:$3/5$。

4.作业

  1. B4016 树的直径,模板题,难度:$1/1$。
  2. P3304 [SDOI2013] 直径,难度:$3/5$。
  3. P4408 [NOI2003] 逃学的小孩,难度$4/5$

5.闲话

蒟蒻不才,膜拜大佬,如果文章有什么问题,请在评论区@我。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/890073.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图吧工具箱PC电脑工具(图拉丁吧硬件检测工具箱) v2025.01 中文绿色便携版

点击上方蓝字关注我 前言 图吧工具箱(大家通常叫它图拉丁吧硬件检测工具箱)是一个完全免费、开源、没有广告的硬件检测工具集合。这个工具箱是专门为喜欢自己动手组装电脑(DIY)的朋友们,特别是图钉社区的成员们制作的。里面包含了好多常用的硬件测试和检测小工具,特别适合…

在Windows上安装UOS打印机

因自己放在办公室发挥余热的Windows电脑即将寿终正寝,所以这两天试着将包括文件打印在内的基础网络服务逐渐转移到UOS主机上。 参照官方教程一通操作,Windows始终在添加打印机页面找不到同个局域网下的UOS主机,点击疑难解答则提示“该设备或资源未设置为接受端口“文件和打印…

是不是长大之后便很难再专注了?

写在前面 1100 字 | 专注 | 思考 | 逆向思维 正文“上课要专心。”我们似乎总能听见大人们这样要求我们。不过坦白地说,我真正体验过全神贯注的时刻,就那么一两次。看起来这篇文章,似乎又要大吐苦水,谈论生活怎样艰辛困苦,各种科技产品怎样将人碎片化之类。并不是。 恰恰相…

2025牛客寒假算法基础集训营3补题笔记

比赛链接 题目难度顺序大致为:\(A、M、F、L、C、\) \(easy\):\(A、M、F、L、C\) 太难了这场。。。E题卡了3个多小时。。。 A.智乃的博弈游戏 题意 有\(n\) 个石头,两人轮流取石头。每次能取小于石头个数且与石头个数互质的数量,当某人取时只有一颗石头则获胜。问先手是否可…

HTTPS 与 HTTP 的区别在哪?

HTTP与HTTPS作为互联网数据传输的核心协议,其通信机制与安全特性深刻影响着现代网络应用的可靠性与用户体验。它们有什么不同?HTTPS到底安全在哪里?HTTP与HTTPS作为互联网数据传输的核心协议,其通信机制与安全特性深刻影响着现代网络应用的可靠性与用户体验。本文将解析两者…

Featurewiz-Polars:一种强大且可扩展的特征选择解决方案,适用于XGBoost

前言:“Featurewiz-Polars”是一个用于特征工程的 Python 库,结合了特征选择和特征生成的功能。它基于“Polars”,这是一个高性能的 DataFrame 库,用于处理大型数据集,具有类似 Pandas 的 API 但更高效,尤其在处理大数据时。Featurewiz-Polars 专注于通过自动化方式,快速…

USACO2025FEB Gold T1 T2 题解

T3 等会补USACO2025FEB Gold 题解 赛时先看 T1,感觉很可做,推了一下发现能把问题转成在基环树上 dp。但我一下子没太想明白怎么解决“在基环树上找环”这个世纪难题,大概在一个小时 20 分钟的时候写完代码(值得一提的是这次罕见地一次就过了编译),交上去发现 WA 了大约一…

Windows10/Windows11系统快速安装杜比音效经典版教程指南

点击上方蓝字关注我 前言 在当今的数字娱乐时代,音频质量已经成为我们追求的重要部分。杜比音效,凭借其卓越的音质和沉浸式的听觉体验,成为了众多音频爱好者的首选,安装杜比音效经典版,提升电脑音频体验。无论是对于追求极致音质的音乐发烧友,还是渴望在观影和游戏时获得…

No.12 HTML5--新增标签

一、HTML5和HTMLHTML5是 HTML 最新的修订版本,2014年10月由万维网联盟(W3C)完成标准制定。 在 HTML5出现之前,我们一般采用 DIV+CSS 布局我们的页面。但是这样的布局方式不仅使我们的文档结构不够清浙,而且不利于搜索引擎爬虫对我们页面的爬取。为了解决上述缺点,HTML5新增…

ICLR 2025 | 无需训练的Token级 DiT加速方法

前言 本文分享 ICLR 2025 论文 ToCa: Accelerating Diffusion Transformers with Token-wise Feature Caching,提出的 ToCa 模型通过 token 粒度的缓存方法,实现了图像和视频生成模型上无需训练的两倍以上的加速。 欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最…

Ⅱ.数据的机器级表示

章节导论: 2.1 位和数据类型 2.1.1 信息最小单位-位(0和1)(如高电压(0.9-1.1v)和低电压(0.0-0.2v)) 2.1.2 数据类型 2.2 整数 2.2.1 无符号整数 位置计数法:通过各个位置的数字不同的权重来表示一个数字 如286=2*102+8*101+6 ,在这个十进制系统中基数为10,同理,二进…

生产CPU打满问题排查

运行好好的系统,突然Prometheus告警,CUP使用率95%告警。赶紧排查一下。 一、先用top -H 命令查看占用CUP高的是哪个进程,看到PID:32518 的进程占用cup过高 二、用top -Hp 32518看看占用资源最多的线程 三、用jstack 32518 > 32518.jstack.log 命令把线程堆栈打印出来四、使…