题意整理:
基本题意
给出一棵具有 个结点的无根树,给出每条边的边权 ,表示两个结点之间的距离。
可以任意选择一个结点作为起点,按照一定的顺序访问每个结点至少一次,问经过的路径的总长度最少是多少。输出这个答案。
数据范围与性质
。
。
注意最后的答案是 的,因此可能超出 范围。
求解目标的转化
我们的路径会经历若干条边。
我们可以简单推断出,题目求解的方案中,每条边至少经过 次。证明如下:
- 每条边经过至少 次: 反证法。如果存在一条边没有被经过,那么不妨在树上断掉这条边,树就分裂成了两个联通块,不可能存在一条路径一次性访问过所有结点。
我们考虑一下最优的路径方案是什么样的。
最优路径一定具有一个起点,我们令其为 ,并且把这棵树看成以 为根的有根树。
我们要从 出发,访问整颗子树的每个结点至少一次。
然后接下来,该路径一定会从 出发走向其一个未访问过的子结点 (如果存在的话),走到 之后,我们可以把接下来的流程看成一个子问题:从 出发,访问以 为根的子树每个结点至少一次(图1)。
然后接下来,访问以 为根的子树完毕后,有两种可能:一个是从 回到 ,然后继续考虑从 访问其他子结点的流程(图2)。
另一个,如果 是从 出发访问的最后一个子结点,说明其他子结点的子树都已经被访问完毕了,那么我们可以不必从 回到 了。
因此,如果根结点选定,从上至下分析可知:最后一个结点访问之后,不需要再向上回溯 (图3) ,因此找出一个深度(距离根结点的距离,用 表示)最大的叶子结点 ,那么 就是以 为根时最短的总路径长度。
由于根结点 可以任意选,而对于任意的 都可以看作是两端分别为 的一条树链。我们要最小化答案,其实就是最大化 ,也就是找到树上的最长树链。
于是我们的求解目标转为了一个经典的树上问题——树上最长链问题,即树的直径。
于是这道题的大致思路:找出树的直径 ,最后输出答案为 。
解法1:暴力换根搜索
【知识点】树的遍历,
【算法】简单的搜索
实现与分析
树上的路径只有 条,我们不妨枚举所有的起点,然后从起点出发遍历整棵树,求出起点。
采用 即可,用 表示起点到 的距离,对于结点 的子结点 ,有 ,其中 表示从 到 的边的长度。
具体细节见代码。
参考代码(会 TLE,通过13/15的数据)
/** * struct Point { * int x; * int y; * }; */ #include <vector> class Solution { public: /** * 最短距离 * @param n int整型 * @param Edge Point类vector * @param val int整型vector * @return long长整型 */ static const int MAXN = 110000; vector <int> son[MAXN];//用于存放每个点链接的结点 vector <int> edge_len[MAXN];//用于存放对应边的长度 long long dep[MAXN]; void dfs(int x,int fa)//存父亲结点防止向父亲结点递归 { for(int i=0;i<son[x].size();i++) { int v=son[x][i]; if(v==fa)continue; dep[v]=dep[x]+edge_len[x][i];//求出 v 的dep dfs(v,x);//递归 } } long long solve(int n, vector<Point>& Edge, vector<int>& val) { // write code here long long ret=0; long long lenth=0;//用于存放最长链长度 for(int i=0;i<Edge.size();i++)//建图,双向边.包括边的指向和边长. { son[Edge[i].x].push_back(Edge[i].y); son[Edge[i].y].push_back(Edge[i].x); edge_len[Edge[i].x].push_back(val[i]); edge_len[Edge[i].y].push_back(val[i]); ret += 2*val[i];//先累加所有边权的两倍到 ret 上 } for(int i=1;i<=n;i++) { dep[i]=0;//以i为根,因此其dep初始化为0 dfs(i,0);//求解所有结点的 dep for(int j=1;j<=n;j++) lenth=max(lenth,dep[j]);//保留最长链到变量 lenth 中 } return ret-lenth;//返回答案 } };
因为从每个起点遍历整棵树是 的,需要枚举 个不同的起点,因此时间复杂度是 ,思路简单,但会超时。 因为每次遍历可以重复使用数组,并且 深度最大达到 因此只达到 的空间复杂度。
解法2:树dp
【算法】树的遍历,动态规划
实现与分析
树上dp是树的直径的一种经典解法。
首先我们把这棵树看作是以 为根的有根树。
我们的答案是求解最长树上路径,而所有的树上路径都可以看作两个端点 之间的简单路径,并且路径一定经过两者的 (最近公共祖先) (图4)。
那么,树上每个点作为 的时候,经过它的最长路径,就是在其子结点中选两个向下延伸路径最长的,然后把两条路径拼起来。将树上每个点作为 考虑之后,取其中最长的路径,就是答案了。
我们考虑通过 处理再计算的方式求解每个结点作为 的最长路径长度。
状态设计:
定义 表示在以结点 为根的子树中,从 出发的最长简单路径的长度。
特殊地,如果不存在满足定义的路径,则取值为 。
状态转移:
,其中 是 的子结点, 表示从 到 的边的长度。
答案构造:
令 表示 的子结点,并且它们已经按照 排好了序。
那么显然, 就是结点 作为 的最长路径长度。
通过树上 实现即可。
参考代码(时间复杂度 )
/**c++ * struct Point { * int x; * int y; * }; */ #include <vector> #include <algorithm> class Solution { public: /** * 最短距离 * @param n int整型 * @param Edge Point类vector * @param val int整型vector * @return long长整型 */ static const int MAXN = 110000; vector <int> son[MAXN];//用于存放每个点连接的结点 vector <int> edge_len[MAXN];//用于存放对应边的长度 long long lenth=0;//用于存放最长链长度 long long dp[MAXN];//dp数组 static bool cmp(int x,int y)//从大到小排序 { return x>y; } void dfs(int x,int fa)//存父亲结点防止向父亲结点递归 { for(int i=0;i<son[x].size();i++) { int v=son[x][i]; if(v==fa)continue; dfs(v,x);//递归 dp[x]=max(dp[x],dp[v]+edge_len[x][i]);//状态转移 } for(int i=0;i<son[x].size();i++)//把子结点改成其dp值+到x的边权,然后排序取最大的两个。 if(son[x][i]==fa) son[x][i]=-1; else son[x][i] = dp[son[x][i]] + edge_len[x][i]; sort(son[x].begin(),son[x].end(),cmp); if(fa)son[x].pop_back();//把父亲结点的影响删除 if(son[x].size()>=2)//更新答案。细节,不能越界。 lenth=max(lenth,0ll+son[x][0]+son[x][1]); else if(son[x].size()>=1) lenth=max(lenth,0ll+son[x][0]); } long long solve(int n, vector<Point>& Edge, vector<int>& val) { // write code here long long ret=0; lenth=0; memset(dp,0,sizeof(dp));//初始化dp数组 for(int i=0;i<Edge.size();i++)//建图,双向边.包括边的指向和边长. { son[Edge[i].x].push_back(Edge[i].y); son[Edge[i].y].push_back(Edge[i].x); edge_len[Edge[i].x].push_back(val[i]); edge_len[Edge[i].y].push_back(val[i]); ret += 2*val[i];//先累加所有边权的两倍到 ret 上 } dfs(1,0); return ret-lenth;//返回答案 } };
只需要遍历一遍树, 深度最大是 的,总空间复杂度 。但是如果要对子结点排序,时间复杂度可达 ,但也可以扫描两遍子结点取最大的两个值,时间复杂度 .