最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
给定一棵树,树中包含 $n$ 个 节点 和 $n−1$ 条 无向边,每条边都有一个权值 $w_{edge_i}$
请在树中找到一个点,使得该点到树中其他结点的 最远距离 最近
分析
这个问题是 树形DP 中的一类 经典模型,常被称作 换根DP
同样,先来想一下如何暴力求解该问题:先 枚举 目标节点,然后求解该节点到其他节点的 最远距离
时间复杂度为 $O(n^2)$,对于本题的 数据规模,十分极限,经测试只能过 $6/10$
考虑如何优化求解该问题的方法
思考一下:在确定树的 拓扑结构 后单独求一个节点的 最远距离 时,会在该树上去比较哪些 路径 呢?
- 从当前节点往下,直到子树中某个节点的最长路径
- 从当前节点往上走到其父节点,再从其父节点出发且不回到该节点的最长路径
此处就要引入 换根DP 的思想了
换根DP 一般分为三个步骤:
- 指定任意一个根节点
- 一次dfs遍历,统计出当前子树内的节点对当前节点的贡献
- 一次dfs遍历,统计出当前节点的父节点对当前节点的贡献,然后合并统计答案
那么我们就要先 dfs 一遍,预处理出当前子树对于根的最大贡献(距离)和 次大贡献(距离)
处理 次大贡献(距离) 的原因是:
如果 当前节点 是其 父节点子树 的 最大路径 上的点,则 父节点子树 的 最大贡献 不能算作对该节点的贡献
因为我们的路径是 简单路径,不能 走回头路
然后我们再 dfs 一遍,求解出每个节点的父节点对他的贡献(即每个节点往上能到的最远路径
两者比较,取一个 max 即可
时间复杂度 为 $T(2n) = O(n)$
闫氏DP分析法
状态表示—集合$f_{1/2,i}$: 对于树中 $i$ 号节点,其子节点的贡献为$f_{1,i}$,父节点的贡献为 $f_{2,i}$
状态表示—属性$f_{1/2,i}$: 贡献值最大
状态计算—$f_{1/2,i}$
$$
\begin{cases}
f_{1,i} = \max\{f_{1,i_{c} }\}\\\
f_{2,i} = \max\{f_{1,i_{p}}~~, f_{2,i_{p}}\} \quad(son_{i_p}\ne i)
\end{cases}
$$
Code
时间复杂度: $O(n)$
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, M = N << 1, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], up[N];
int s1[N], s2[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs1(int u, int father)
{
// d1[u] = d2[u] = -INF; //这题所有边权都是正的,可以不用初始化为负无穷
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
dfs1(j, u);
if (d1[j] + w[i] >= d1[u])
{
d2[u] = d1[u], s2[u] = s1[u];
d1[u] = d1[j] + w[i], s1[u] = j;
}
else if (d1[j] + w[i] > d2[u])
{
d2[u] = d1[j] + w[i], s2[u] = j;
}
}
// if (d1[u] == -INF) d1[u] = d2[u] = 0; //特判叶子结点
}
void dfs2(int u, int father)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
if (s1[u] == j) up[j] = w[i] + max(up[u], d2[u]); //son_u = j,则用次大更新
else up[j] = w[i] + max(up[u], d1[u]); //son_u != j,则用最大更新
dfs2(j, u);
}
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d", &n);
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs1(1, -1);
dfs2(1, -1);
int res = INF;
for (int i = 1; i <= n; i ++ ) res = min(res, max(d1[i], up[i]));
printf("%d\n", res);
return 0;
}
Code(直接暴力换根)
时间复杂度: $O(n^2)$ 通过数据 $60\%$
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, M = N << 1, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int dfs(int u, int father)
{
d1[u] = d2[u] = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == father) continue;
int dist = dfs(j, u) + w[i];
if (dist >= d1[u]) d2[u] = d1[u], d1[u] = dist;
else if (dist > d2[u]) d2[u] = dist;
}
return d1[u];
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d", &n);
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
int res = INF;
for (int i = 1; i <= n; i ++ ) res = min(res, dfs(i, -1));
printf("%d\n", res);
return 0;
}
是y总和彩铅老师教会了我dp
Y总负责总体思路,彩铅哥负责教会我细节原理
彩老师我爱你!(:
有没有可能父节点的次大路径上也包含当前节点呢?(不是很理解
不会的,我的理解:问题转换一下应该就是问 一个节点 的 最大路径和次大路径 有没有可能经过同一个节点?没可能的,先把这个点放在最高点(既看作根节点)来计算最大值和次大值,次大值更新的条件是找到一个比当前最大值还要大的路径,则把之前的最大值赋值给次大值,现在假设根节点有三个子节点,三个子节点的最大路径分别为d1 + w[1] < d2 + w[2] < d3 + w[3],所以根节点的最大路径在第三个子节点中,假设 数值意义上的次大路径 也在第三个子节点中,从思想上来看,我们计算第三个子节点的最大路径(d3)时,是以第三个子节点为根节点去算的,就是说第三个子节点的第二大路径并不会影响最高节点的次大值,只会影响最高节点的最大值(d3 + w[3]),也就是说对于最高节点来说,每个子节点都有一个它的最大值,从里面挑出最大的就是最大路径,第二大的才是我们说的次大路径
可能说的有点乱,可以把AcWing 1072. 树的最长路径 这题的样例数据 2 6 8 改成 2 6 10,然后自己模拟一遍就清楚了
现在懂了,次大路径和最大路径一定不是一个子树
orz写的太清楚了 我理解的.s2虽然没用到但是加上的话整个代码更加有一种对称的简约~!!%%%%
彩铅大哥%%%% orz 清晰的离谱
彩铅大佬是真的强啊
暴力直接换根不用记录次大值。
s2没必要算吧
s2数组是不是没用
这里的次长路 是否不是真正意义上的次长路 而是不经过最长路节点时的最长路 因为每次更新只更新了最长路 不会更新次长路
我只能说我看了两个上午终于看懂了换根dp是什么了(๑><๑)
为什么dfs1中是先dfs1(j,u)再判断条件,而dfs2()中是先判断再dfs2()
#和
彩铅哥爱你啊
~i这个什么意思啊 为什么要这样?
取反,-1在计算机里面的存储是111…1111,取反之后就是0。所以~i在for循环里面就是i!=-1的意思
菜逼真是太强了!Orz
我的铅笔哥,我的铅笔哥
每次y总的视频没看懂,就来看彩铅大佬的题解,太牛了!!
1.7树形DP
最大路径、最大贡献、最长路径 把我绕晕了
https://www.acwing.com/solution/content/6825/ 没看懂的话可以试试看这个
时间复杂度怎么分析呢?
同搞不清这个时间复杂度怎么分析的qaq…
若暴力DFS的话,需要把每个节点当成根
dfs一遍所有的子树
那么最坏情况下时间复杂度为$O\bigg(n\times (n-1)\bigg)$,若用树形DP的话,只需要任选一个节点当作根,然后dfs一遍所有的子树
,最坏情况下时间复杂度为$O\bigg(2(n-1)\bigg)$因为树形DP只需要
dfs_down和dfs_up
一遍,所以时间复杂度是$O\bigg(2(n-1) \bigg)$