目录
题目解析
算法原理
图的存储
算法实现
题目解析
题目解析
给定一颗树,树中包含n个结点(编号)和n-1条无向边。请找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
什么是重心?
重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中节点的数量的最大值最小,那么这个节点被称为树的重心。
补充:树和图的关系
树本质上是一种特殊的图,它对应的是无环连通图。也就是说,在树中任意两个节点之间都存在路径(连通性),并且不存在回路(无环)。
树与一般图相比,有着如下性质:
- 一般的图可以有环,节点之间的连接关系比较复杂。比如在一个有向图中,边是有方向的,从一个节点到另一个节点的路径可能受到边的方向限制;而树作为无向图,边没有方向限制,并且由于其无环的特性,从任意节点到另一个节点的路径是唯一的。
- 从连通性角度看,树是保持图连通的最小结构。如果从树中去掉任何一条边,就会导致图不再连通,会划分出两个或更多的连通分量;而对于一般的连通图,可能存在冗余的边,去掉某些边后仍然可以保持连通。
重心举例
如下图,我们得到一个树结构:
若我们删除节点1,那么我们会得到三个连通图,如下:
- 所谓的各个连通块中节点的最大值,指的是连通块之间的对比,在上图中,连通块节点数量最大值是4,即中间的连通块节点数量
重心是指我们遍历这张图中所有的节点,并尝试删除节点后所形成连通块节点数量的最大值是最小的。
举一个非重心的例子:如下图
我们尝试删除节点2后,所形成的连通图一共有如下3个:
很明显,连通块节点数量的最大值是6。
而尝试删除1的连通块节点数量的最大值是4,所以2一定不是树的重心
实际上,在上图中一共存在着两个重心,它们分别是1和4,感兴趣可以自己尝试算算
树的重心的性质
- 树中所有点到某个点的距离和中,到重心的距离和是最小的,如果有两个距离和,他们的距离和一样。
- 删除重心后所得的所有子树,节点数不超过原树的1/2,一棵树最多有两个重心;
- 把两棵树通过一条边相连,新的树的重心在原来两棵树重心的连线上。
- 一棵树添加或者删除一个节点,树的重心最多只移动一条边的位置。
- 一棵树最多有两个重心,且相邻。
算法原理
我们要求得一棵树得重心,那么较为简单得一种方式是尝试计算每一个节点如果被删除后,所得的连通块数量的最大值,并取计算完后的最小值。
下图删除节点4为例,思考一下,若删除节点4后,所形成的连通块有哪些?
很明显,连通块只会出现在两种情况:
- 根节点为4的树的所有子树都是连通块
- 整棵树的根,刨除4这个子树所形成的连通块
那么如何获得根节点为4的树的所有子树的节点数量,以及4所对应的树的节点数量呢
我们只需DFS即可:
- 遍历到9,9无子树,向上返回1
- 遍历到3,3的子树的连通块节点数量为1,那么3对应的连通块节点数量就为2,3向上返回2
- 遍历到6,6无子树,向上返回1
- 遍历到4,4的子树的连通块节点数量为2+1=3,所以4对应的连通块中节点数量为3+1=4(包含4自身)
如何获得整棵树的根(抛出删除节点4对应子树)的连通块数量呢?
直接通过n-i计算即可
其中n表示的是总的节点数量,在上图中n即为9。
其中i表示的是以被删除节点为根的子树的连通块数量,在上图中i即为4
图的存储
图的存储我们采用邻接表
- 邻接表是图(包括有向图和无向图)的一种链式存储结构。它主要用于存储图中节点之间的连接关系。
- 对于图中的每个顶点,都有一个单链表,链表中的节点存储了与该顶点相邻接的顶点信息
举例
假设,值为1的点指向了2、4、7。那么我们只需要构建一个单链表,表头节点是1,其余的表示的是1指向它
若每一个节点都有一张单链表,用来表示它所指向的元素,那么该存储结构我们称之为邻接表
若是无向图,那么当1指向2时,还需要添加一条2指向1的边
单链表的实现
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int ne[N], e[N], idx;
//ne存储的是next指针指向的节点编号
//e存储的是节点编号所对应的值
//idx用于分配一个唯一的节点编号
int main()
{
int n; //n是节点的数量
cin >> n;
//下标为0的点是头节点
ne[0] = -1;//初始化单链表,-1来表示空节点
idx = 1; //节点编号从1开始,因为0已经分配给头节点了
for (int i = 0; i < n; ++i)
{
int x;
cin >> x;
e[idx] = x; //分配一个节点编号给x
ne[idx] = ne[0];//当前节点指向头节点指向的节点
ne[0] = idx++;//头节点指向当前节点
}
for (int i = ne[0]; i != -1; i = ne[i]) printf("%d ", e[i]);
return 0;
}
邻接表的实现
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N], ne[N], e[N], idx;
void add(int a,int b)
{
//1 3 表示1指向3,即h[1]头插3
//添加一条a->b的边
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int main()
{
int n; //边的数量
cin >> n;
memset(h, -1, sizeof h); //初始化邻接表
for (int i = 0; i < n; ++i)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
return 0;
}
算法实现
第一步:根据题目要求,我们首先把输入进来的无向图保存起来,题目的输入如下:
- 第一行:输入的是n
- 第2-n行:输入两个数字a,b。表示a和b之间有一条无向边
具体代码参考邻接表的实现
第二步:深度优先遍历
dfs中有一个参数u在dfs期间我们也一并更新最终的结果,定义变量ans表示最终的返回值
dfs函数要完成的功能是获得以u为根的子树形成的连通块节点数量,并更新根节点ans
注意:由于每一个节点我们只需要遍历一次,所以我们定义一个bool数组st,若st[i]=true,表示该节点已经被遍历过了,反之未遍历过
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N], ne[N], e[N],st[N], idx;
int ans = N,n; //n表示边的数量 ,ans表示删除结果
void add(int a,int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int dfs(int u)
{
int res = 0; //res表示删除点u后连通块节点数量最大值
int sum = 1; //sum表示要返回给上一层的以u为根的子树节点数量,初始化为1因为至少有u一个节点
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
int s = dfs(j);
res = max(res, s); //连通块节点数量最大值可能出现在子树中
sum += s;
st[j] = false;
}
}
res = max(res, n - sum); //最大值可能出现在根节点抛去u子树后的连通块节点数量
ans = min(res, ans); //u可能是重心,更新一下
return sum;
}
int main()
{
cin >> n;
memset(h, -1, sizeof h); //初始化邻接表
for (int i = 0; i < n-1; ++i)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
dfs(1);
printf("%d", ans);
return 0;
}