2021-02-12

PAT (Advanced Level) Practice

1021 Deepest Root (25 分)

一开始只想到暴力法,即枚举所有节点作为树根,计算树高,得出最大树高。显然有很多重复枚举,最简单的,比如u为根的树,最深的节点为v,则显然以v为根的树,最深的节点肯定也是u,所以算了以u为根,就不用算以v为根。《算法笔记》的参考程序给出了一个定理,直接给出了高效的求法,实现倒不难,就要理解这个定理为什么是对的,要费点脑子。

这个定理的关键是要注意到,所有的最长路径(即树的直径),都有一段公共路径,因此以这条公共路径为分界线,所有节点被分为三类:

  1. 公共路径上的点。
  2. 公共路径一侧的点,即不在公共路径上。
  3. 公共路径另一侧的点,也不在公共路径上。

第一次选取的任一个点,无论是属于上述哪一类,经过DFS求最深的后代节点,都会得到2或者3里的点,而且,可以证明,这样的点必然是最深根(Deepest Root,即使得树最高的根节点),具体不证。故算法的第一步,得到了最深根集合的一部分节点,并且全部位于2或者3里面。然后可以发现,最深根集合的节点,只可能位于2或者3里面,不可能在1里面。这样通过第一步,我们已经得到了最深根集合的一个子集,那就是位于2或者3的那部分子集,接下来,算法的第二步从第一步得到的子集的任意一个节点出发,继续找它的最深后代集合,显然,得出的最深后代集合只能在第一步集合的对面(如果第一步得到的是属于2的集合,则第二步得到的就是属于3的集合,反之亦然),这样一来,两步所得到的集合,正好是最深根集合的两部分(如果1的公共路径退化为一个节点,则这两个集合有交集,否则无交集)。已经论证了,最深根只可能在2,3里面取,所以这两个集合的并集,一定是全部的答案。

#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include <set>
#include <cassert>
#include <vector>
using namespace std;

const int INF=1000000000;

#define MAXN (10000+5)
int N;
vector<int> Adj[MAXN];
bool vis[MAXN];
// 有最大距离的节点对的端点。 
set<int> Ans;
 
void DFS(int u, int depth, int& hi) {
	vis[u]=true;
	if (depth > hi) {
		hi=depth;
	}
	for (int i=0;i<Adj[u].size();++i) {
		int v=Adj[u][i];
		if (!vis[v]) {
			DFS(v, depth+1, hi);
		}
	}
}

int TreeHi(int root) {
	// 计算root为根的树的高度。
	int ans=0;
	fill(vis, vis+N+1, false);
	DFS(root, 0, ans);
	return ans; 
}

/* run this program using the console pauser or add your own getch, system("pause") or input loop */

int father[MAXN]; // 并查集检查联通性。
void Init() {
	for (int i=1;i<=N;++i) {
		father[i]=i;
	}
}
int Find(int x) {
	int a=x;
	while (x != father[x]) {
		x=father[x];
	}
	while (a != father[a]) {
		int temp=father[a];
		father[a]=x;
		a=temp;
	}
	return x;
}
void Union(int a, int b) {
	int faA=Find(a);
	int faB=Find(b);
	
	if (faA != faB) {
		father[faA]=faB;
	}
}
void Print() {
	for (int i=1;i<=N;++i) {
		printf("%d: ", i);
		for (int j=0;j<Adj[i].size();++j) {
			printf("%d ", Adj[i][j]);
		}
		printf("\n");
	}
}

// 联通分量数。 
int FindComp() {
	int ans=0;
	for (int i=1;i<=N;++i) {
		// 看看N个节点,那个是根。
		if (father[i]==i) {
			++ans;
		} 
	}
	assert(ans>0);
	return ans;
}

/*
这个O(VE+V^2)的算法居然过了,说明PAT的测试数据比较弱。
*/

/*
有定理加持的高效算法:
1. 选择任意节点作为根,进行DFS,获取深度最大的节点集合,记为A。
2. 任取A的一个元素,以此为根,进行DFS,获深度最大的节点集合,记为B。
3. 所求的最深根节点集合记为 A 并 B。

复杂度:O(V+E),即DFS的复杂度。
*/

/*
对某个节点进行DFS遍历,记录下最深的节点,记录在vi中。
*/ 
void DFS2(int u, int depth, int& maxDepth, vector<int>& vi) {
	vis[u]=true;
//	printf("u %d maxd %d\n", u, maxDepth);
	
	if (depth > maxDepth) {
		// 注意更新全局最大值。 
		maxDepth=depth;
		vi.clear();
		vi.push_back(u); // u的深度达到新高。 
	} else if (depth == maxDepth) {
		// 达到当前最大深度。
		vi.push_back(u); 
	}
	for (int i=0;i<Adj[u].size();++i) {
		int v=Adj[u][i];
		if (!vis[v]) {
			DFS2(v, depth+1, maxDepth, vi);
		}
	}
}

// 找某节点为根的最深叶节点集合。
// 即以某节点为根的树,深度为树高的叶节点集合。 
void FindDeep(int root, vector<int>& vi) {
	int deep=0;
	fill(vis, vis+N+1, false);
	DFS2(root, 0, deep, vi);
}
 
/*
计算最深根(Deepest Roots)集合。
使用定理的版本。 
*/
void Compute2() {
	vector<int> A, B;
	// 计算集合A,即某节点出发的最深节点集合。 
	FindDeep(1, A);
	
	assert(A.size());
	// 计算集合B,即A的某元素出发的最深节点集合。 
	FindDeep(A.front(), B);
	
	// 计算AB的并集。 
	set<int> ans;
	vector<int>::iterator it;
	for (it=A.begin();it!=A.end();++it) {
//		printf("%d\n", *it);
		ans.insert(*it);
	}
//	puts("");
	for (it=B.begin();it!=B.end();++it) {
//		printf("%d\n", *it);
		ans.insert(*it);
	}
	// ans 即为最深根节点集合,并且排序。
	for (set<int>::iterator it=ans.begin();it!=ans.end();++it) {
		printf("%d\n", *it);
	} 
}

/*
暴力版本。
*/ 
void Compute() {
	int ans=-1;
	for (int v=1;v<=N;++v) {
		// 计算以v为根的树高。 
		int hi=TreeHi(v);
		// 更新全局最大值。 
		if (hi > ans) {
			ans=hi;
			Ans.clear();
			Ans.insert(v);
		} else if (hi == ans) {
			Ans.insert(v);
		}
	}
	
	set<int>::iterator it;
	// 从小到大输出。 
	for (it=Ans.begin();it!=Ans.end();++it) {
		printf("%d\n", *it);
	}
}

int main(int argc, char** argv) {
	scanf("%d", &N);
	int M=N-1;
	/*
	注意,树的一个条件,即E=V-1已经满足,只需要检查联通性是否满足。
	因为联通且E=V-1的必定是树,不用检查环了。
	*/ 
	Init(); // 并查集初始化。 
	while (M--) {
		int a,b;
		scanf("%d%d", &a, &b);
		Adj[a].push_back(b);
		Adj[b].push_back(a);
		// 无向图。 
		Union(a, b);
	}
	int n=FindComp();
	if (n != 1) {
		// 不是联通图,不是树。
		printf("Error: %d components\n", n); 
	} else {
		Compute2();
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值