题目
题目描述:
牛妹有一张连通图,由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);
} 
京公网安备 11010502036488号