AcWing 1072. 树的最长路径(树形DP)
- 一、题目:
- 二、思路:
- 三、代码:
- 四、树形DP
- 1、状态表示
- 2、状态转移
- 3、循环设计
- 4、初末状态
- 5、代码实现
一、题目:
二、思路:
为了方便,我们利用下面这个图做讲解:
这颗树的最长路径必定经过的是图中的点,因此,**我们可以去枚举经过图中每个点的最长路径,然后再这些路径中选出一个最长的作为答案。**那么我们需要怎么做呢?
我们这里采用的是DFS(深度优先搜索),如果对DFS不了解的话,作者建议去看一下之前对DFS算法的专门讲解:第十三章 DFS与BFS(保姆级教学!!超级详细的图示!!) 和 第十四章 图的存储及图的DFS(超级详细!!逐行解析!!)
很多同学不会写DFS,其实根本原因在于每道题中DFS递归函数的含义都是不一样的,而想要写出DFS就要明白DFS在每道题语境中的含义。
在本道题中,我们将DFS写成下面的样子:
int dfs(int u,int father)
**这个函数的作用是返回到u点最远的点的距离。**那么这么定义的作用是什么呢?
我们看下面的图:
由于我们求的是最大距离,而最大距离必须要比较以后才能知道,因此就需要求出到u点的所有距离。然后我们选出前两个最大的。这样这两个距离和u点就构成了一个经过u点的最大路径。(详细过程如图中所示)
也就是说我们在求到u点的最大距离的过程中顺便求出了经过该点的最长路径。
并且,我们的DFS会去遍历每个点,所以经过每个点的最长路径都会求出来,此时我们只需要定义一个全局变量记录所有最长路径中最长的一个即可。
这里还有几个细节处理,前两个最大的距离组成了我们经过u点的最大距离,但是如果最大的距离中是负的,那么我们就不加上这个距离,因为加上的话只会减少。
另外,由于题目中是无向边,所以在遍历u的子节点的时候,也会遍历到u的父节点,所以我们参数中多写一个父节点,这样做的目的就是防止循环遍历造成死循环。
三、代码:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int N=1e4+10;
vector<pii>g[N];
int n;
int ans;
int dfs(int u,int father)
{
int d1=0,d2=0;
for(int i=0;i<g[u].size();i++)
{
auto j=g[u][i];
if(j.first==father)continue;
int d=dfs(j.first,u)+j.second;
if(d>=d1)
d2=d1,d1=d;
else if(d>d2)
d2=d;
}
ans=max(ans,d1+d2);
return d1;
}
int main()
{
cin>>n;
for(int i=0;i<n-1;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
g[b].push_back({a,c});
}
dfs(1,-1);
cout<<ans<<endl;
return 0;
}
四、树形DP
通过上面的代码我们发现,我们的时间复杂度是 O ( n ) O(n) O(n)的,因为我们的DFS函数遍历了树中的每一个点,而我们的主函数中只调用了一次DFS,所以总共的时间复杂度是 O ( n ) O(n) O(n)。
这个时间复杂度很低,打破了我们对DFS的认识,按照以前对DFS的理解,提到DFS我们就会想到两个字:暴力。并且我们对DFS的时间复杂度也有着刻板地印象:指数级别。
这道题之所以时间复杂度低,就是因为这道题虽然代码上看起来是深搜,实际上是DP的思想。
那么什么是DP的思想呢?
DP的思想就是:利用子问题解决当前问题,从小规模的问题开始解决,利用小规模的问题的解解决大规模的问题。
那么我们利用DP的分析过程去解释刚刚的DFS思路:
1、状态表示
f [ 1 / 2 ] [ u ] f[1 / 2][u] f[1/2][u],其中 f [ 1 ] [ u ] f[1][u] f[1][u]表示树中的其他点到达该点的最大距离, f [ 2 ] [ u ] f[2][u] f[2][u]表示树中的其他点到达该点的次大的距离。
2、状态转移
我们发现
f
[
1
]
[
u
]
f[1][u]
f[1][u]可以利用b,x,c三个点转移过来,那么写成然后在三种情况中取一个最大值。可以写成:
f
[
1
]
[
u
]
=
m
a
x
(
f
[
1
]
[
b
]
+
w
[
b
]
,
f
[
1
]
[
x
]
+
w
[
x
]
,
f
[
1
]
[
c
]
+
w
[
c
]
)
f[1][u] = max\big(f[1][b]+w[b],f[1][x]+w[x],f[1][c]+w[c]\big)
f[1][u]=max(f[1][b]+w[b],f[1][x]+w[x],f[1][c]+w[c])
那么
f
[
2
]
[
u
]
f[2][u]
f[2][u]的转移也是类似的,只不过是从三个里面挑出一个次大的。
3、循环设计
循环的话,我们发现这道题由于是一个树形的图,所以很难写成我们 f o r for for循环的样子。因此,我们这里只能用DFS来表示这里的循环。
于是我们就发现了树形DP中其实分析的思路和别的DP问题是一致的,只不过是循环的方式从for循环改成了DFS。
4、初末状态
初始化的话,就全部初始化为0就好了,有人说为什么不弄成负数,其实很简单,如果有一条路线是负的,那我不选它就好了。因此我们能始终保证最优解大于等于0。
末尾状态的话,其实也好表示,这道题求的是最长直径,我们只需要将 f [ 1 ] [ u ] f[1][u] f[1][u]和 f [ 2 ] [ u ] f[2][u] f[2][u]加在一起即可。
然后在所有的
f
[
1
]
[
i
]
f[1][i]
f[1][i]中求出一个最大值。
5、代码实现
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10, M = 2 * N;
int h[N], e[M], ne[M], w[M], idx;
int f[3][N];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c;
h[a] = idx++;
}
void dfs(int u, int father)
{
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(j == father)continue;
dfs(j, u);
if(f[1][j] + w[i] >= f[1][u])
{
f[2][u] = f[1][u];
f[1][u] = f[1][j] + w[i];
}
else if(f[1][j] + w[i] >= f[2][u])
f[2][u] = f[1][j] + w[i];
}
}
int main()
{
memset(h, -1, sizeof h);
int n;
cin>>n;
for(int i = 0; i< n - 1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c),add(b, a, c);
}
int res = 0;
dfs(1,-1);
for(int i = 1; i <= n; i ++ )
{
res = max(f[1][i]+f[2][i], res);
}
cout<<res<<endl;
return 0;
}