本篇讲解PAT甲级“1021 Deepest Root”题目的解法
下面是题目:
1021 Deepest Root (25)(25 分)
A graph which is connected and acyclic can be considered a tree. The height of the tree depends on the selected root. Now you are supposed to find the root that results in a highest tree. Such a root is called the deepest root.
Input Specification:
Each input file contains one test case. For each case, the first line contains a positive integer N (<=10000) which is the number of nodes, and hence the nodes are numbered from 1 to N. Then N-1 lines follow, each describes an edge by given the two adjacent nodes' numbers.
Output Specification:
For each test case, print each of the deepest roots in a line. If such a root is not unique, print them in increasing order of their numbers. In case that the given graph is not a tree, print "Error: K components" where K is the number of connected components in the graph.
Sample Input 1:
5
1 2
1 3
1 4
2 5
Sample Output 1:
3
4
5
Sample Input 2:
5
1 3
1 4
2 5
3 4
Sample Output 2:
Error: 2 components
题目大意:
给一个图,求以哪些节点为起点的形成的树的深度达到最大,返回所有的这些根节点。
如果不连通或不为树(成环),返回连通分量数
算法思想(普通版本):
1.用图的BFS层次遍历,起点为树根,深度为遍历的层数
2.visit用遍历层数代替,标定是否访问过的同时可记录这是第几层!
3.当判断到邻接点的访问层数非零且<本层-1时,才是成环!
4.能成为答案的节点(最深树的根节点),必然是端点!
一开始读题没有注意到题目给的边数量是节点数N减去1,居然用EOF来判定输入是否结束。
根据本算法思想,我花了1小时6分钟写出了该版本满分代码:
#include<stdio.h>
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
int N;
vector<vector<int>> G; //表示i和G[i][*]邻接,0号下标不使用
bool hasCircle = false; //是否成环
vector<int> visitH; //0表示未遍历
int getHbyBFS(int sV) {
queue<int> Q;
Q.push(sV);
visitH[sV] = 1;
int H; //遍历深度
while (!Q.empty())
{
int V = Q.front(); Q.pop();
H = visitH[V];
for (auto adjV = G[V].begin(); adjV != G[V].end(); adjV++) {
if (0 == visitH[*adjV]) {
Q.push(*adjV);
visitH[*adjV] = H + 1;
}
else if(visitH[*adjV]<H-1) hasCircle = true;
}
}
return H;
}
int main()
{
cin >> N;
G.resize(N + 1);
int v1, v2;
while (EOF != scanf("%d %d", &v1, &v2)) {
if (v1 > N || v2 > N || v1 <= 0 || v2 <= 0)break;
G[v1].push_back(v2);
G[v2].push_back(v1);
}
int maxH = 0;
vector<int> rV; //能作为最深树的节点
//先看是否成环或多联通分量
visitH.resize(N + 1, 0); //0表示未遍历
for (int v = 1; v <= N; v++) {
if (0 == visitH[v]) {
maxH = getHbyBFS(v);
rV.push_back(v);
}
}
if (rV.size()>1 || hasCircle) { //成环或多联通分量
printf("Error: %d components\n", rV.size());
return 0;
}
for (int v = 2; v <= N; v++) {
if (G[v].size() != 1)continue; //不是端点的跳过
visitH.clear();
visitH.resize(N + 1, 0); //0表示未遍历
int h = getHbyBFS(v);
if(h>= maxH) { //有效记录!
if (h > maxH) { //刷新记录!
maxH = h;
rV.clear();
}
rV.push_back(v);
}
}
for (int i = 0; i < rV.size(); i++)
printf("%d\n", rV[i]);
system("pause");
return 0;
}
虽然此版代码能满分通过,但是有一个测试点消耗时间达1000ms!
最优时间复杂度版本——O(N),N为节点数
此版本只需遍历两次DFS即可求出所有最高树的树根。
原先只是将某节点作为树根,求该树的深度,暴力地暴力所有潜在的节点而求出所求。
这样思考一下:如果找的树根不是端点,而此根有子树T1、T2、.....,假设子树T1、T2即为该端点的最深的两个子树,那么将T1或T2其中一个的最深叶子节点"拎起来"作为树根,就可以得到更高的树。
这里我们定义最高树的任一最深叶子节点到树根的回溯路径称为“最高树主链”,如果上面找的端点是最高树主链上的节点,那么T1或T2的最深叶子节点无疑是答案所求的“最高树树根”之一。
这么说来可能很抽象,打个比方,将这个图的节点当成一个环状钥匙扣(尽可能小),每条边都是等长的细线(绝对牢固且尽可能长),将节点环扣连接起来。那么取其中一个节点作为树根的树的高度就是将此节点的环扣"拎起来",其他环扣随重力下垂,整个链下垂的高度就与树高成正比了。
而整个图能找到的最高树树高,就是比较所有节点的最长的两个支路(子树)长度(树高)之和再+1的值,取其最大值。
而求支路即子树的树高,可以递归进行。
如此只需遍历一遍,就能确定这个节点有无过“最高树主链”的倾向了(有时候这个节点是“最高树主链”之一的途径节点,但在程序的一次遍历中没有确定出来,但没有关系,因为其祖先能帮他确定)
说了这么多,还是要看具体怎么做!如下图源代码:
#include<stdio.h>
#include<vector>
#include<queue>
using namespace std;
int N, maxH=0;
class Node {
public:
int H1=0, H2=0, maxL=0; //只要被遍历过,则不为0
};
vector<Node> nodes;
vector<bool> visit;
vector<vector<int>> G;
vector<bool> dye; //此为true则为所求的能成为最高树的根节点
//测量子树长度DFS
void measureDFS(int v) {
visit[v] = true;
auto &self = nodes[v];
for (auto adjV = G[v].begin(); adjV != G[v].end(); adjV++) {
if(visit[*adjV]==false){
measureDFS(*adjV); //遍历子树
if (nodes[*adjV].H1 >= self.H1) { //平了或刷新H1
self.H2 = self.H1;
self.H1 = nodes[*adjV].H1;
}
else if (nodes[*adjV].H1 > self.H2) { //只刷新了H2
self.H2 = nodes[*adjV].H1;
}
}
}
self.H1++;
self.maxL = self.H1 + self.H2;
self.H2++; //算上自己要+1
}
//单纯DFS,求连通分量用
void DFS(int v) {
visit[v] = true;
for (auto adjV = G[v].begin(); adjV != G[v].end(); adjV++) {
if (visit[*adjV] == false) {
DFS(*adjV); //遍历子树
}
}
}
//开始染色
void dyeDFS(int v,bool need_dye) {
visit[v] = true;
auto &self = nodes[v];
int H;
if (self.maxL == maxH) { //自己和两个排名并列一、二的子树,构成了最高树主链
if (G[v].size() <= 1) { //自己是端点
dye[v] = true; //为自己染色
}
H = self.H2 - 1;
}
else if (need_dye) { //自己和最高子树,构成祖先的最高子树的一部分
if (G[v].size() <= 1) { //自己是端点
dye[v] = true; //为自己染色
}
H = self.H1 - 1;
}
else {
H = maxH; //这意味着不可能有 if (nodes[*adjV].H1 >= H) 能成立
}
for (auto adjV = G[v].begin(); adjV != G[v].end(); adjV++) {
if (visit[*adjV] == true)continue; //防止重复遍历
if (nodes[*adjV].H1 >= H) { //子树长度达标,可构成最高树主链的一部分,则需要染色
dyeDFS(*adjV, true);
}
else {
dyeDFS(*adjV, false); //否则只能当节点自身maxL达到maxH时才能重启染色
}
}
}
int main()
{
scanf("%d", &N);
nodes.resize(N + 1);
visit.resize(N + 1,false);
G.resize(N + 1);
dye.resize(N + 1,false);
for (int i = N - 1; i > 0; i--) { //Then N-1 lines follow
int v1, v2; scanf("%d %d", &v1, &v2);
G[v1].push_back(v2);
G[v2].push_back(v1);
}
int DFS_times = 1;
measureDFS(1);
for (int v = 2; v <= N; v++) { //查是不是一棵树
if(visit[v]==false) { //没有一次过DFS,说明不是一棵树
DFS(v); DFS_times++;
}
}
if (DFS_times>1) {
printf("Error: %d components\n", DFS_times);
}
else {
fill(visit.begin(), visit.end(),false);
for (int v = 1; v <= N; v++) { //查maxH
if (nodes[v].maxL >maxH) {
maxH = nodes[v].maxL;
}
}
dyeDFS(1,false); //还是从1开始遍历,保持一致
//染色的端点即为所求
for (int v = 1; v <= N; v++) {
if (true == dye[v])
printf("%d\n", v);
}
}
return 0;
}
可以上PAT官网的测试点测试!结果如下图,最多仅花费9ms!比网上大多数版本快了整整100倍!
因为理论上,必须遍历一遍才能确定节点相对图的位置 ,所以此解法时间复杂度O(N)已经是最优时间复杂度的解法了。如果有什么问题或意见请提出,欢迎大神指正。