文章目录
题目
原题链接:https://www.acwing.com/problem/content/848/
题目描述
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1条无向边。请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a和点 b之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围
1≤n≤105 (n大于等于1,小于等于10的5次方)
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例
4
先画出题目中的树:
简要分析
根据题目的描述,树的重心就是指删除树中的某一个结点后剩余各个连通子树的结点数目的最大值最小。下面来分析一下:
若删除结点1,则剩余三个连通子树,很容易看出中间那颗子树的结点数目最多有4个,所以剩下的最大连通子树的结点数目为4
若删除结点2,则剩余两个结点数目为1的子树和一个结点数目为6的子树,即剩下的最大连通子树的结点数目为6
若删除结点3,则剩余一个结点数目为1的子树和一个结点数目为7的子树,即剩下的最大连通子树的结点数目为7
若删除结点4,则剩余一个结点数目为3的子树和一个结点数目为5的子树,即剩下的最大连通子树的结点数目为5
...
一直往下枚举就可以得出删除结点1后剩下的最大连通子树的结点数目最小,也就是说结点1就是树的重心。
解题思路
根据分析可以知道必须要把树中的全部结点都枚举一遍才能才能算出哪个结点是树的重心,所以需要对树进行深搜dfs
如果删除结点4,下面蓝色部分是结点4的子树,根据题目已经知道树的总结点数为n,所以只需要算出蓝色部分(结点4的子树)结点数目sum,同时算出最大子树的结点数size,就可以算出上面蓝色部分(连通树)的结点数目n-sum,再比较n-sum和size的大小,得出剩下的最大连通子树的结点数目。
题目要求的是将重心删除后,输出剩余子树的最大连通子树的结点数目,所以用深搜dfs算出每个结点删除后剩下的最大连通子树的结点数目,然后再比较出最小值即可。
代码实现
树是一种特殊的图(无向图),存储图有邻接矩阵法,邻接表法,链式邻接表法和链式前向星法等,这里选择的是邻接表法来存储图,由于树是无向图,所以存储边的时候正反两条都得存。
存储图得代码实现:
c
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+10;数据范围是10的5次方
//以有向图的格式存储无向图,所以每个节点至多对应2n-2条边
int e[2*N];//e[i]的值是编号,是下标为i节点的编号。
int ne[2*N];//ne[i]的值是下标,是下标为i的节点的next节点的下标。
int h[2*N];//h[i]存储的是下标,是编号为i的节点的next节点的下标,比如编号为1的节点的下一个节点是4,那么我输出e[h[1]]就是4
int idx;单链表指针(下标)
bool vis[2*N];//记录节点是否被访问过,访问过则标记为true
int n;//n个节点
int ans=N;//表示重心的所有的子树中,最大的子树的结点数目
//a所对应的单链表中插入b a作为根
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
int main()
{
memset(h,-1,sizeof(h));//初始化h数组 -1表示尾节点
memset(vis,0,sizeof(vis));//初始化vis数组,都还没访问过
cin >> n;
// 树中是不存在环的,对于有n个节点的树,必定是n-1条边
for(int i=1;i<n;i++)
{
int a,b;
cin >> a >> b;
add(a,b);
add(b,a);//存储无向图
}
//打印邻接表
for(int i=1;i<=n;i++)
{
cout << i;
for(int j=h[i];j!=-1;j=ne[j])
{
cout << "->" << e[j];
}
cout << "\n";
}
return 0;
}
运行结果:
接下来就是深搜dfs,任取一点u,若以u为重心,则分为两类:一类是以u为根节点的子树,一类是根节点u上面的部分。需要计算出以u为根节点的最大子树的结点数和u上面的部分的结点数,然后取二者最大值即可。还要定义一个布尔数组(bool)vis记录每个结点是否访问过,如果已经访问过,就无需再访问,避免向上查找。
从根到叶,再从叶回到根,总是在返回时收集子树的信息(这里是结点的数目),从小到大更新每个结点的信息size[u],sum[u],更新ans。
因为u是任取的一点,所以在遍历每个点是都会得到一个ans,只需要取最小值即可。
AC代码
cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+10;数据范围是10的5次方
//以有向图的格式存储无向图,所以每个节点至多对应2n-2条边
int e[2*N];//e[i]的值是编号,是下标为i节点的编号。
int ne[2*N];//ne[i]的值是下标,是下标为i的节点的next节点的下标。
int h[2*N];//h[i]存储的是下标,是编号为i的节点的next节点的下标,比如编号为1的节点的下一个节点是4,那么我输出e[h[1]]就是4
int idx;单链表指针(下标)
bool vis[2*N];//记录节点是否被访问过,访问过则标记为true
int n;//n个节点
int ans=N;//表示重心的所有的子树中,最大的子树的结点数目
//a所对应的单链表中插入b a作为根
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
int dfs(int u)
{
vis[u]=true;//标记u这个点被搜过
int sum=1;//记录以u为根的子树的结点数,根结点也要包含,所以初始化为1
int size=0;//记录以u为根的最大子树的结点数
for(int i=h[u];i!=-1;i=ne[i])//i是边的编号
{
int j=e[i];//j是u的邻接点
if(vis[j]) continue;//避免向上查找
int g=dfs(j);//g是以j为根的子树的结点数
size=max(size,g);//记录以u为根的最大子树的结点数
sum+=g;//累加以u为根的各个子树的结点数
}
int sump=max(size,n-sum);//最大连通子树的结点数目
ans=min(ans,sump);//更新答案,找到删除每个结点后的最大连通子树的结点数目的最小值
//ans=min(ans,max(size,n-sum));//更新答案
return sum;
}
int main()
{
memset(h,-1,sizeof(h));//初始化h数组 -1表示尾节点
memset(vis,0,sizeof(vis));//初始化vis数组,都还没访问过
cin >> n;
// 树中是不存在环的,对于有n个节点的树,必定是n-1条边
for(int i=1;i<n;i++)
{
int a,b;
cin >> a >> b;
add(a,b);
add(b,a);//存储无向图
}
dfs(1);//任取一个点,开始深搜
cout << ans << "\n";
return 0;
}
对以上内容有异议的欢迎大家来讨论,希望对大家有帮助,多多支持哦,以后也会更新有关算法的内容