1021. Deepest Root (25)-PAT 最优时间复杂度版

本篇讲解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倍!

 

最高只花费9ms!测试图

因为理论上,必须遍历一遍才能确定节点相对图的位置 ,所以此解法时间复杂度O(N)已经是最优时间复杂度的解法了。如果有什么问题或意见请提出,欢迎大神指正。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值