树的重心
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。数据范围
1≤n≤105
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4 -
题目来源:https://www.acwing.com/problem/content/848/
题目分析:
- 看重心的定义:删除重心后,剩余连通分量中含有节点数最大者比删除其他节点造成的连通分量含有节点数最大值更小
- 通过定义,我们发现重心是和其余每个树上节点比较出来的,比较内容就是删除这个节点后各连通分量中点数最大者的点数
- 删除该节点后的连通分量只有两部分:子树 & 大树删除该树后剩余部分
使用maxx维护这两部分中的最大值即可 - 计算删除该树的剩余部分:大树 - 1 - 所有子树中节点数
- 为了方便求出删除该树节点的剩余部分,我们将树上的每个节点都赋值为所有其子树节点和
求子树节点和,DFS。 - 下面来看DFS的递归过程如何完成为每个节点赋值为子树节点和
算法原理:
模板算法:
- 传送门:DFS
树的重心:
- 可能上面的题目分析没有解释清楚问题,下面详细说说:
1. 删除节点后的连通分量:
- 所有内容分3部分:
- 该点本身,点数为1
- 该点子树,点数为3 & 1
- 删除该点为树根的树后剩余部分,点数为大树点数14 - 1 -3 -1 == 9
- 点数最多的连通分量出自于各个子树 或 删除子树和该点后剩余的部分
2. 各个连通分量的点数计算:
- 删除该点为树根的树后剩余部分的点数计算,依靠于总点数n - 1 - 该点子树中点数
- 各子树的点数计算,依靠于递归计数,将子树的点数赋值给树根
3. 从目的出发设计DFS:
- 如上所属,递归一方面要将子树点数赋值给树根
- 另一方面可以在递归遍历节点时,就完成若删除该点剩余连通分量的最值统计
- 用maxx维护更新这个最值即可
- 图示:
4. 树的存储形式:
- 输入格式为:a b,则表示a b存在一条边
- 最多n个节点,n-1条边
- 该树采用邻接表存储即可
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
//静态链表5件套,h val ne idx memset(h)
int h[N], val[2*N], ne[2*N], idx;
int n;
bool st[N];
void insert(int x, int y){
val[idx] = y;
ne[idx] = h[x];
h[x] = idx++;
}
int minn = N;
int bfs(int x){
st[x] = 1;
//sum是所有子树节点数,res是所有子树中节点最多的子树的节点数
int sum = 0, res = 0;
for(int i = h[x]; i!=-1; i=ne[i]){
if (!st[val[i]]){
//使用tmp查看单一子树的节点数,为的是res比较得出最多的子树节点
int tmp = bfs(val[i]);
res = max(res, tmp);
sum += tmp;
}
}
//res在子树节点 和 除去该树剩余部分作比较
res = max(res, n-1-sum);
minn = min(minn, res); //若要输出具体节点,在此处进行类似与堆的down()操作的试探是否改变
//最容易忘记的就是这里,一定要return
return sum+1;
}
int main(){
memset(h, -1, sizeof(h)); //-1补码都是1,memset()以字节为单位填充
cin >>n;
for(int i=0; i<n-1; i++){
int x, y;
cin >>x >>y;
//无向图是特殊的有向图
insert(x, y);
insert(y, x);
}
bfs(1); //无所谓从1,只要是树上节点都可以视作树根
cout<< minn;
return 0;
}
代码误区:
1. 为什么本题的树以邻接表的形式存储?
- 树可否以邻接表存储?
树属于特殊的图,无环连通图,理论上可以使用图来存储 - 本题侧重于遍历树,即不重不漏即可
邻接表搭配st[]标记数组,可以做到不重不漏 - 树是无向图,使用邻接表需要双链都增加节点
2. 为什么从1号节点开始dfs?
- 本题没有声明树根是哪个节点,说明树根是谁或者从谁开始遍历不影响连通分量的划分
- 也可以从树上任意一个节点开始遍历,比如现在给定下面的树:
- 现在从8号开始遍历这颗树:
- 虽然树的遍历形状发生了变化,但是节点和节点之间的关系没有变化,去掉某一节点剩余的连通分量也没有变化
- 总结:
st[N]标记避免了回头导致无穷递归
此时无论选择哪个节点作为开头,都只改变了树根
未改变树的绝对形状和节点的相对关系,更没有改变去掉某点的连通分量
若指定树根,其实是指定了树的遍历起始点
本篇感想:
- 未强调树根的树可以借助st[]数组随便指定树根
- 本题难度在递归的写作 以及 静态邻接表的使用
- 看完本篇博客,恭喜已登 《筑基境-初期》
距离登仙境不远了,加油