树形DP总结
就是在树上进行的动态规划,一般是递归进行的,使用 DFS 来更新 DP 数组
结合下面这个例题进行理解:
直径数量问题
题目描述
给出一棵树,求树的直径及其数量(最长简单路径的长度和数量)。
输入格式
第一行一个整数 n , 0≤n≤105,
接下来有 n−1 行,每行 a,b 代表 a 到第 b 有一条长度为 1 的边。
1≤a,b≤n
输出格式
输出两个数,分别表示树的直径和数量。
样例输入
5
5 1
1 2
2 3
2 4
样例输出
3 2
题解
首先求以 i 为根节点的子树的直径(直径这个词就已经默认是最长的了),使用 DFS 进行更新
对于一棵以 i 为根节点的子树,其直径有以下三种情况,
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQ4M7f8w-1657079942423)(C:\Users\YYYYYKN\AppData\Roaming\Typora\typora-user-images\image-20220530002850986.png)]](https://i-blog.csdnimg.cn/blog_migrate/c040c926d4c02cd13d24f8816ebb2619.png)
定义一个数组 zuidashendu 记录每个节点的最大深度
那么遍历 i 的每一个孩子节点 j ,当 最大深度[i] + 最大深度[j] + 1 > 以 i 为根的子树的直径 的时候, 以 i 为根的子树的直径就进行更新,更新为 最大深度[i] + 最大深度[j] + 1 。
因为:
1、若直径是第一种情况,就需要找深度最大的那个孩子节点,把他的深度加上 1 就是直径了。那么 最大深度[i] 这个时候等于 0 (因为如果直径是这种情况,那么父亲节点 i 只会有 j 一个孩子节点,否则直径都可以通过这个父亲节点 i 扩展到其他孩子节点上,直径就不会是这第一种情况了。刚要遍历到第 j 个孩子节点,还没更新 i 的最大深度,所以此时 i 的最大深度还是 0 ),则 最大深度[j] + 1 就得到了这个直径
2、若直径是第二种情况,那么节点 i 的两个深度最大的孩子节点再连接上这两个孩子节点各自与父亲节点 i 相连的那两条边就构成了直径——相当于父节点在遍历到第 j 个子节点之前的最大深度加上第 j 个孩子节点的最大深度再加 1
3、若直径是第三种情况,那么注意,我们设置了一个变量来记录当前找到的直径(也就是下文代码中的 max_d),如果直径是第三种情况的话,使用 (最大深度[i] + 最大深度[j] + 1) 这个计算公式计算出来的结果都不可能大于当前找到的直径 max_d ,就不会更新这个 max_d ,所以 max_d 仍然保持着是不经过根节点的情况。
根据上述更新公式 最大深度[i] + 最大深度[j] + 1 > 以 i 为根的子树的直径 时就更新为 最大深度[i] + 最大深度[j] + 1 ,一直 DFS 直到根节点,这时的 max_d 就是答案了。
那怎么求解直径的方案数(也就是下文代码中的 num_d )呢?
首先还要设置一个数组 zuidashendushuliang 来记录每个节点的最大深度数量。
在遍历节点 i 的每个孩子节点 j 的时候:
1、若是 j 的最大深度加上 1 大于了 i 的最大深度,就更新 i 的最大深度为 j 的最大深度加上 1 ,并更新 i 的最大深度的数量是 j 的最大深度的数量。
2、若是 j 的最大深度加上 1 等于 i 的最大深度,就把 i 的最大深度的数量自加 1 。
由上述方法就能求出每个节点的最大深度和最大深度数量。
接下来,最大深度[i] + 最大深度[j] + 1 > 以 i 为根的子树的直径 时就更新直径的同时,也要把方案数 num_d 更新为最大深度数量[i] * 最大深度数量[j](因为可以有全排列的方案数来得到这个直径)。而当最大深度[i] + 最大深度[j] + 1 == 以 i 为根的子树的直径时就可以把方案数 num_d 自加 1 ,因为这个时候又找到了一种得到最大直径的方案。
特别注意!!!最大深度数量的初始化!!!!要先初始化每个节点最大深度的数量为1,不然一乘就会直接把直径的方案数变成 0 了,比如下图这种情况,求到最上面那个根节点的时候,根节点和子节点的最大深度方案数都是 0 ,最后的方案数就是 0 ,显然不对!!!
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8oFO61HG-1657079942425)(C:\Users\YYYYYKN\AppData\Roaming\Typora\typora-user-images\image-20220530012150701.png)]](https://i-blog.csdnimg.cn/blog_migrate/a1f0cfbb19fe060799d7df4a67bf743a.png)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll zuidashendu[1000000] = {0}; //记录节点的最大深度
ll zuidashendushuliang[1000000] = {0}; //记录节点的最大深度的数量
ll max_d = 0, num_d = 0; //记录最大直径和他的数量
vector<vector<int>> tree(1000000);
int vis[1000000] = {0}; //记录这个点有没有被访问过
void dfs(ll s)
{
for (auto &i : tree[s])
{
if (vis[i] == 0)
{
vis[i] = 1;
dfs(i);
//更新直径和直径的方案
if (zuidashendu[s] + zuidashendu[i] + 1 > max_d) //注意这里的形式,可以直接包括三种情况
{
max_d = zuidashendu[s] + zuidashendu[i] + 1;
num_d = zuidashendushuliang[i] * zuidashendushuliang[s];
}
else if (zuidashendu[s] + zuidashendu[i] + 1 == max_d)
num_d += zuidashendushuliang[i] * zuidashendushuliang[s];
//更新当前这棵树的根节点的最大深度和最大深度的数量
if (zuidashendu[i] + 1 > zuidashendu[s])
{
zuidashendu[s] = zuidashendu[i] + 1;
zuidashendushuliang[s] = zuidashendushuliang[i];
}
else if (zuidashendu[i] + 1 == zuidashendu[s])
zuidashendushuliang[s] += zuidashendushuliang[i];
}
}
}
int main()
{
ll n;
cin >> n;
for (ll i = 1; i < n; i++)
{
ll a, b;
cin >> a >> b;
tree[a].push_back(b);
tree[b].push_back(a);
zuidashendushuliang[i] = 1;
}
for (ll i = 1; i <= n; i++) //!注意这里要先初始化每个节点最大深度的数量为1,不然遇到有叶子节点的最大深度就直接把方案数变成0了
zuidashendushuliang[i] = 1;
vis[1] = 1;
dfs(1);
cout << max_d << ' ' << num_d << endl;
return 0;
}
这篇博客介绍了如何利用树形动态规划解决求解树的直径及其数量的问题。通过深度优先搜索(DFS)更新每个节点的最大深度和最大深度数量,从而找出树的直径和对应的方案数。关键在于考虑三种可能的直径情况,并在遍历过程中更新直径和方案数。代码示例展示了具体的实现过程。
2105

被折叠的 条评论
为什么被折叠?



