题目
https://www.patest.cn/contests/pat-a-practise/1021
题意:给出一张图,判断该图能否形成一棵树,若能则求出树的最长简单路(树的直径),否则求出连通分量的个数。
解题思路
本题是树的直径+并查集的应用,似乎是某C9的机试题之一。
解决这道题的整体思路是:
1. 先通过并查集判断连通分量的个数,若连通分量只有1个就说明不存在森林;
2. 对剩下的这些连通顶点做两遍搜索。第一遍任意选一个顶点u做起点,通过BFS找到距离u最远的顶点v,第二遍再从v出发做同样的BFS,搜到的最长路即树的直径,可以得到直径的端点,也就是题目所说的Deepest Root。
关于并查集判断连通分量个数,之前的博客代码里已经用过很多次,不再赘述。
关于求树的直径这种算法,引用一下网上的证明。
原理: 设起点为u,第一次BFS找到的终点v一定是树的直径的一个端点
证明:
(1)如果u是直径上的点,则v显然是直径的终点(反证:假设v不是直径终点,则必定存在另一个点w使得u到w的距离更长,这和BFS搜索到v矛盾)
(2)如果u不是直径上的点,则u到v必然与树的直径相交(假设不相交,要么是森林,要么v不是这趟BFS终点),所以交点到v必然就是直径的后半段。
值得注意的点:
- 为了找到距离u最深的顶点v,在BFS过程中用level数组记录每个顶点的深度,即其前驱节点的深度+1,这样当BFS完成后,level里就记录了所有顶点与u的距离。
- 非常需要小心的是,这两遍BFS都可能会找到多个终点,因此每遍BFS以后都要记录下来;但是有些顶点可能会被记录两次!(测试点1、3、4)举个栗子,若第一遍选择的顶点恰好在直径的中间(或者图只有一个顶点),则两端的顶点都可能是终点,如果用vector来记录就会被push两次,导致重复输出。
- 由于最终输出要求排序,结合上面这点,可以用最简单的布尔数组root[i]来标记i是否为端点,能避免排序和重复的问题。
AC代码
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int maxn = 10005;
vector<int> adj[maxn]; //邻接链表
int n;
int pre[maxn]; //前驱结点
void init() //并查集初始化
{
for (int i = 0; i <= n; ++i)
pre[i] = i;
}
int find_root(int x) //找到x所在连通集的根
{
if (pre[x] == x) //根
return x;
return pre[x] = find_root(pre[x]); //路径压缩
}
int calcComponets() //统计连通分量的数量
{
int cnt = 1;
int f1 = find_root(1), f2;
for (int i = 2; i <= n; ++i) //用顶点1去尝试连通其他节点
{
f2 = find_root(i);
if (f1 != f2) //合并两个连通分量
{
pre[f2] = f1;
cnt++;
}
}
return cnt;
}
int level[maxn]; //记录此次访问中,各顶点相对x的深度
int bfs(int x) //bfs搜索距离x最远的点
{
bool visited[maxn]; //顶点是否访问过
memset(visited, false, sizeof(visited));
queue<int> q;
q.push(x);
level[x] = 0; visited[x] = true;
int max_depth = 0, pre, now;
while (!q.empty())
{
pre = q.front(); //取出顶点作为前驱结点
q.pop();
for (int i = 0; i < adj[pre].size(); ++i)
{
now = adj[pre][i];
if (!visited[now]) //防止前后顶点互相访问
{
q.push(now);
visited[now] = true; //标记访问过
level[now] = level[pre] + 1; //深度+1
if (level[now] > max_depth)
max_depth = level[now];
}
}
}
return max_depth;
}
void max_depthestRoot() //求树的直径:两遍BFS
{
bool root[maxn] = {false}; //记录某个顶点是不是端点,避免排序
//第一遍BFS
int depth = bfs(1);
int pos;
for (int i = 1; i <= n; ++i)
{
if (level[i] == depth) //可能有多个端点
{
pos = i;
root[i] = true;
}
}
//第二遍BFS,从第一遍找到的某个端点
depth = bfs(pos);
for (int i = 1; i <= n; ++i)
if (level[i] == depth)
root[i] = true;
for (int i = 1; i <= n; ++i)
{
if(root[i] == true)
cout << i << endl;
}
}
int main()
{
cin >> n;
//通过并查集找图的连通分量
init();
int x, y, fx, fy;
for (int i = 0; i < n-1; ++i) //输入n-1条边
{
cin >> x >> y;
adj[x].push_back(y); //x和y有边
adj[y].push_back(x);
fx = find_root(x);
fy = find_root(y);
if (fx != fy) //合并连通集
pre[fx] = fy;
}
int connected = calcComponets(); //计算连通分量数
if (connected != 1) //有多个连通分量,不构成树
{
printf("Error: %d components\n", connected);
return 0;
}
max_depthestRoot(); //求树的直径
return 0;
}