PAT (Advanced Level) Practice
1021 Deepest Root (25 分)
一开始只想到暴力法,即枚举所有节点作为树根,计算树高,得出最大树高。显然有很多重复枚举,最简单的,比如u为根的树,最深的节点为v,则显然以v为根的树,最深的节点肯定也是u,所以算了以u为根,就不用算以v为根。《算法笔记》的参考程序给出了一个定理,直接给出了高效的求法,实现倒不难,就要理解这个定理为什么是对的,要费点脑子。
这个定理的关键是要注意到,所有的最长路径(即树的直径),都有一段公共路径,因此以这条公共路径为分界线,所有节点被分为三类:
- 公共路径上的点。
- 公共路径一侧的点,即不在公共路径上。
- 公共路径另一侧的点,也不在公共路径上。
第一次选取的任一个点,无论是属于上述哪一类,经过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;
}