题目
PTA | 程序设计类实验辅助教学平台 (pintia.cn)
本人做法
思路
利用DFS来实现对整个图进行遍历,如果得到的连通分支数>1,则该图与树不等价;
并在DFS的过程中保存能达到的最大深度,最后进行排序以获得相应节点。
代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
#define MAXV 10005
vector<int> adjList[MAXV];
bool visited[MAXV] = {false};
int depth[MAXV];
int N,K;
int DFS(int n,int depth){
visited[n] = true;
int ans = depth;
for(int i = 0;i < adjList[n].size();i++){
if(!visited[adjList[n][i]]){
ans = max(DFS(adjList[n][i],depth+1),ans);
}
}
return ans;
}
void DFS_Trans(){
for(int i = 1;i <= N;i++){
if(!visited[i]){
DFS(i,1);
K++;
}
}
}
int main(){
cin>>N;
int m,n;
if(N==1){
cout<<"1"<<endl;
return 0;
}
for(int i = 1;i <= N;i++){
cin>>m>>n;
adjList[m].push_back(n);
adjList[n].push_back(m);
}
memset(visited,false,sizeof(visited));
DFS_Trans();
if(K>1) {
cout<<"Error: "<<K<<" components";
return 0;
}
memset(visited,false,sizeof(visited));
for(int i = 1;i <= N;i++){
depth[i] = DFS(i,1);
memset(visited,false,sizeof(visited));
}
int maxv = 0;
for(int i = 1;i <= N;i++){
if(depth[i]>maxv) maxv = depth[i];
}
for(int i = 1;i <= N;i++){
if(depth[i]==maxv) cout<<i<<endl;
}
}
但该写法的复杂度较高,可能是数据比较弱吧,能ac掉。一旦数据比较强,写法就会G掉。
考虑好边界,特况特判。
所以还是建议学习下面的更优解法。
更优做法
思路
1.判断图是否等价为树
连通且边数为N-1的图必为一棵树。
而题目中的输入边数已经确定为N-1条,所以可以用并查集,根据最后集合个数是否为1来判断给定数据是否可使图连通。
2.如何找到满足要求的节点
先选择任意一个节点,从该节点来遍历整棵树,获取能达到的最深顶点的集合(记为A),然后从集合A中任意一个节点出发遍历整棵树,获取能达到的最深顶点的集合(记为B)。这样A∪B则是题目中要求的使树高最大的根节点。
证明
从直观上来看,该解法是正确的。但为保证严谨性,还需要进行数学上的证明。
1. 证明:从任意结点出发遍历,得到的根节点一定是所求根结点的一部分
使用反证法。
直径:把最大树高对应的路径拉成一条直线,称为树的直径。
设R为使得树高最大的根节点,那么存在某个叶子结点L,使得R到L的长度即为树的最大数高。
把R到L的路径拉成一条直线(即为该树的直径)。
(图中以线段的长度代表结点间距)
现从任意一个结点X开始遍历,假设遍历得到的最深节点是Y,且Y≠L≠R,那么有OY>OL,所以有OR+OY>OR+OL。那么Y才是由R为根节点的最大树高的对应叶子结点,而这与前提R到L的距离是最大树高矛盾,因此假设不成立。
所以:从任意结点X进行树的遍历,得到的最深结点一定是R或者L,即所求根节点的一部分。
2. 证明:所有直径一定有一段公共重合区域(或是交于一个公共点)
(1)先证明任意两条直径一定相交
假设存在两条长度相同的不相交直径XY,WZ(字母表示结点)
根据树的连通性,一定存在点P,Q是互相可达的,其中P在XY上,Q在WZ上。若如此,则会存在一条更长的直径XZ,WY,这与直径的定义相矛盾,因此假设不成立。
即,任意两条直径一定相交。
(2)再证明所有直径一定有一段公共重合区间或一个公共点
假设三条直径X1Y1,X2Y2,X3Y3相交且不交于同一点,如图所示。则可通过P、Q、R拼接出更长存的直径,这与直径的定义矛盾,因此假设不成立。
有公共重合区域的情况同理可得。
3.证明:所有两次遍历结果的并集为所求的根结点的集合
现在看有公共区域的那个图,如果所选结点在公共区域PQ之间,那么最深节点会是A1~Am或者B1~Bk中的其中一组或是全部。而在第二次遍历时,任取一个根节点即可遍历完整另外一侧所有最深的结点。
同理,如果第一次遍历的结点在P的左侧(或Q的右侧),则最深节点一定是B1~Bk(或是A1~Am),这样在第二次遍历时,任取一个根节点即可遍历完整另外一侧所有的最深结点。
代码
用vector
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 100005;
vector<int> G[N];
bool isRoot[N];
int father[N];
int findFather(int x) {
if (x == father[x])
return x;
int z = findFather(father[x]);
father[x] = z;
return z;
}
void Union(int a, int b) {
int faA = findFather(a);
int faB = findFather(b);
if (faA != faB) {
father[faA] = faB;
}
}
void init(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
}
}
int calBlock(int n) {
int block = 0;
for (int i = 1; i <= n; i++) {
isRoot[findFather(i)] = true;
}
for (int i = 1; i <= n; i++) {
block += isRoot[i];
}
return block;
}
int maxHeight = 0;
vector<int> temp, ans;
void DFS(int u, int h, int pre) {
if (h > maxHeight) {
temp.clear();
temp.push_back(u);
maxHeight = h;
} else if (maxHeight == h) {
temp.push_back(u);
}
for (int i = 0; i < G[u].size(); i++) {
if (G[u][i] == pre)
continue;
DFS(G[u][i], h + 1, u);
}
}
int main() {
int a, b, n;
scanf("%d", &n);
init(n);
for (int i = 1; i < n; i++) {
scanf("%d %d", &a, &b);
G[a].push_back(b);
G[b].push_back(a);
Union(a, b);
}
int block = calBlock(n);
if (block != 1) {
printf("Error: %d components\n", block);
} else {
DFS(1, 1, -1);//从1号结点开始遍历
ans = temp;//此时temp为集合A
DFS(ans[0], 1, -1);
for (int i = 0; i < temp.size(); i++) {
ans.push_back(temp[i]);//此时的temp为集合B
}
sort(ans.begin(), ans.end());
printf("%d\n", ans[0]);
for (int i = 1; i < ans.size(); i++) {
if (ans[i] != ans[i - 1])
printf("%d\n", ans[i]);
}
}
return 0;
}
提醒自己注意的点:
1.并查集一定要记得初始化
2.DFS的两种变式:
(1)如何记录DFS过程中达到的深度
(2)无向图中保存前一个结点保证跳过回去的边
用set
用set容器的话可以避免掉去重的操作。
#include <cstdio>
#include <cstring>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
const int N = 100005;
vector<int> G[N];
bool isRoot[N];
int father[N];
int findFather(int x) {
if (x == father[x])
return x;
int z = findFather(father[x]);
father[x] = z;
return z;
}
void Union(int a, int b) {
int faA = findFather(a);
int faB = findFather(b);
if (faA != faB) {
father[faA] = faB;
}
}
void init(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
}
}
int calBlock(int n) {
int block = 0;
for (int i = 1; i <= n; i++) {
isRoot[findFather(i)] = true;
}
for (int i = 1; i <= n; i++) {
block += isRoot[i];
}
return block;
}
int maxHeight = 0;
set<int> temp,ans;
void DFS(int u, int h, int pre) {
if (h > maxHeight) {
temp.clear();
temp.insert(u);
maxHeight = h;
} else if (maxHeight == h) {
temp.insert(u);
}
for (int i = 0; i < G[u].size(); i++) {
if (G[u][i] == pre)
continue;
DFS(G[u][i], h + 1, u);
}
}
int main() {
int a, b, n;
scanf("%d", &n);
init(n);
for (int i = 1; i < n; i++) {
scanf("%d %d", &a, &b);
G[a].push_back(b);
G[b].push_back(a);
Union(a, b);
}
int block = calBlock(n);
if (block != 1) {
printf("Error: %d components\n", block);
} else {
DFS(1, 1, -1);
ans = temp;
DFS(*ans.begin(), 1, -1);
for (set<int>::iterator it = temp.begin();it!=temp.end();it++) {
ans.insert(*it);
}
for (set<int>::iterator it = ans.begin();it!=ans.end();it++) {
printf("%d\n", *it);
}
}
return 0;
}
注意:set正好是默认升序的