题目
题目描述:
牛妹有一张连通图,由n个点和n-1条边构成,也就是说这是一棵树,牛妹可以任意选一个点作为根,根的深度deproot为0。
对于任意一个非根的点,我们将他到根节点路径上的第一个点称作他的父节点,例如1为根。
1-4的:路径为1-3-5-4时,4的父节点是5,并且满足对任意非根节点,深度=父节点的深度+1。
整棵树的价值为所有结点的深度值之和。
牛妹希望这棵树的W最小,请你告诉她,选择哪个点可以使W最小。
输入描述:
第一行,一个数n。
接下来n-1行,每行两个数x,y,代表x-y是树上的一条边。
输出描述:
一行,一个数,最小的W。
解析一:树形dp(换根)
第一次写这种题目,所以在参考了邓老师的提示下,我总结一下~
- 首先正常操作,我们先用树的孩子表示法存储好树。
- 接下来我们应该求出每一个节点作为根时的权和。
- 最后我们选择最小的那个权和进行输出就行了。
这里我们呢要选择用dp以降低时间复杂度:
树形dp(换根)法:
- 这道题要我选一个点使深度和最小。
- 首先我们简单的dfs或bfs都会,但是暴力肯定不行,会超时的。
- 所以我们的目标就是用某一个根节点,推出当ta的子节点作为根时的权和。
- 那我们应该怎么推呢,我们能发现,如果是根节点的子结点当根节点时:该子结点的分支(包括自己)深度都会减一;而其他节点深度都会加一。
- 按照这个说法我们就能得到递推公式:F[y]=F[x]−cnt[y]+(n−cnt[y])。(我们假设F为某节点为根节点时的权和,cnt为某节点分支的节点数量,n为总节点数量)
换根图解
AC代码:树形dp(换根)
#include <iostream> #include <vector> using namespace std; typedef long long ll; #define js ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); const int MAX = 1e6 + 7; vector<int> Node[MAX];//树的孩子表示法 int F[MAX], depth[MAX], cnt[MAX];//F[i]表示当i为根时的权和;depth表示每个结点的深度;cnt表示每个结点的子树大小 void dfs(int x, int fa);//初始化depth数组和cnt数组 void dp(int x, int fa, int n);//换根dp求解每个结点的权和(F) int main() { js; int n; cin >> n; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; Node[u].push_back(v); Node[v].push_back(u); } //树的孩子表示法初始化 dfs(1, -1); F[1] = 0; for (int i = 1; i <= n; i++) F[1] += depth[i]; dp(1, -1, n); //求出各结点权和 int min = 1; for (int i = 2; i <= n; i++) if (F[min] > F[i]) min = i; //求出最大权和 cout << F[min] << endl; return 0; } void dfs(int x, int fa) { //x为当前节点,fa为父节点 if (fa == -1) depth[x] = 0; else depth[x] = depth[fa] + 1; cnt[x] = 1; for (auto c : Node[x]) if (c != fa) { dfs(c, x); cnt[x] += cnt[c]; } } void dp(int x, int fa, int n) { if (fa != -1) F[x] = F[fa] - cnt[x] + (n - cnt[x]); for (auto c : Node[x]) if (c != fa) dp(c, x, n); }
解析二:重心查找
这个嘛,我感觉也还有点小兰。
简单来说,就是找出我们要的那个那个根节点,然后再dfs或bfs一遍求出权和。
所以我们这里要用到一个性质:
- 树中所有点到某个点的距离和中,到重心的距离和是最小的,如果有两个重心,他们的距离和一样。
然后我们怎么用呢?这是关键
- 因为我们要距离和最小,我们同时也可以认为,就是这棵树最矮!
- 所以我们要选出可以让这颗树最矮的节点。
- 这里我们用到计算子树结点数的方法:我们需要进行一些操作,使得根节点的左子树和右子树的高度差最小。
那我们就开始操作!
- 我们先选取某一个节点为根节点,并用一个标记(pos)记住我们要的节点
- 我们先将pos放在叶节点上,然后按照中序遍历对每个节点进行计算。
- 与此同时,我们也正在计算每个节点的分支节点数(cnt)。(这里是同步进行的。为了方便理解计算过程,我们也可以直接认为我们已知每个点的分支节点数。)
- 我们先选取出我们正在操作的节点中,cnt最大的分支。(根节点的左右两边应该是不相上下的,所以我们先选最大)
- 然后再和我们已知的pos节点进行比较,保留较小者。(因为最大最小进行折中:就是最大里面的最小。所以就能得到一个折中值了)
ps:这里我觉得难理解的就是最大再最小的操作了,我也是理解学习了大佬的。
(所以我们可以这样想,总会有一个节点左右两边差不多。所有点的分支最大值一定是超过一半的,再从这些最大值里面选最小的,当然就正好是一半的那个啦)
AC代码:重心查找
#include <iostream> #include <vector> using namespace std; typedef long long ll; #define js ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); const int MAX = 1e6 + 7; vector<int> Node[MAX];//树的孩子表示法 int depth[MAX], cnt[MAX], son[MAX];//F[i]表示当i为根时的权和;depth表示每个结点的深度;cnt表示每个结点的子树大小;son表示子树的大小,最终是超过一半的最小数量 int pos = 0, ans = 0;//重心位置 void find(int x, int fa, int n);//查找重心 void dfs(int x, int fa);//初始化depth数组和cnt数组 int main() { js; int n; cin >> n; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; Node[u].push_back(v); Node[v].push_back(u); } //树的孩子表示法初始化 find(1, -1, n); dfs(pos, -1); cout << ans << endl; return 0; } void find(int x, int fa, int n) { cnt[x] = 1; son[x] = 0; for (auto c : Node[x]) if (c != fa) { find(c, x, n); cnt[x] += cnt[c]; son[x] = max(son[x], cnt[c]); } son[x] = max(son[x], n - cnt[x]); if (!pos) pos = x; else if (son[x] < son[pos]) pos = x; } void dfs(int x, int fa) { //x为当前节点,fa为父节点 if (fa == -1) depth[x] = 0; else depth[x] = depth[fa] + 1; ans += depth[x]; for (auto c : Node[x]) if (c != fa) dfs(c, x); }