记录118
cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN=310;
int fa[MAXN],dep[MAXN];//fa[i]记录i的直接领导(父节点),dep[i]记录i的深度(层级)
bool vis[MAXN];//DFS遍历时用来标记节点是否被访问过
vector<int> ch[MAXN];//ch[i]记录i的所有直接下属(子节点)
int getDep(int x){// 递归计算节点 x 的深度(根节点0的深度为0)
return x==0?0:getDep(fa[x])+1;
}
void dfs(int x){// DFS深度优先遍历:标记节点 x 以及 x 的所有下属(即 x 的整棵子树)
vis[x]=1;
for(int y:ch[x]) dfs(y);//for循环的简写方式
}
// 检查节点 x 是否能管理 vec 中的所有参与者
// 逻辑:如果 x 能管理所有人,那么 vec 里的每个人一定都在 x 的子树(管辖范围)里
bool check(int x,int n,const vector<int> &vec){// 加上 const:既防止了函数内部误改数据,又配合 & 避免了数据拷贝,是更专业、更高效的写法。
for(int i=0;i<=n;i++) vis[i]=0;// 每次检查前,先把标记数组清空
dfs(x);// 标记 x 能管理的所有人
for(int y:vec){
if(vis[y]==0) return 0;
}// 遍历所有参与者,如果有任何一个人没被标记,说明 x 管不了他
return 1; // x 能管理所有人
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin>>n;//输入员工数量
for(int i=1;i<n;i++){
cin>>fa[i];// 读入领导关系并建树
ch[fa[i]].push_back(i);// 把 i 加入到 fa[i] 的下属列表中
}
for(int i=1;i<n;i++) dep[i]=getDep(i);// 预处理计算出每个节点的深度
int q;
cin>>q;// 需要安排的合作场数
while(q--){
int m,minD=n+1;// minD用来记录当前组参与者中的最小深度(职位最高者的深度)
cin>>m;// 参与本次合作的员工数量
vector<int> vec(m);// 构建m大小的数组vec
for(int i=0;i<m;i++){
cin>>vec[i];// 读入参与者
minD=min(minD,dep[vec[i]]);// 找出其中深度最浅的那个值(minD)
}
// 倒序枚举节点(从编号大到编号小),寻找符合要求的主持人
for(int i=n-1;i>=0;i--){
// 剪枝与判断:
// 1. dep[i] <= minD:领导 i 的深度必须小于等于参与者中最浅的深度(领导必须在最上面)
// 2. check(i, n, vec):领导 i 必须能管理 vec 中的所有人
if(dep[i]<=minD&&check(i,n,vec)){
cout<<i<<"\n";
break;
}
}
}
return 0;//结束程序
}
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
题目传送门
https://www.luogu.com.cn/problem/P10109
突破口
某公司有 N 名员工,编号从 0 至 N−1。其中,除了 0 号员工是老板,其余每名员工都有一个直接领导。我们假设编号为 i 的员工的直接领导是 fi。
该公司有严格的管理制度,每位员工只能受到本人或直接领导或间接领导的管理。具体来说,规定员工 x 可以管理员工 y,当且仅当 x=y,或 x=fy,或 x 可以管理 fy。特别地,0 号员工老板只能自我管理,无法由其他任何员工管理。
现在,有一些同事要开展合作,他们希望找到一位同事来主持这场合作,这位同事必须能够管理参与合作的所有同事。如果有多名满足这一条件的员工,他们希望找到编号最大的员工。你能帮帮他们吗?
思路
1. 建模:将公司层级转化为"树"
- 节点与边 :每个员工是一个节点,直接领导关系构成了一条有向边(从父节点指向子节点)。老板
0是整棵树的根节点。 - 管理关系 :题目规定"x 可以管理 y,当且仅当 x=y,或 x=fy,或 x 可以管理 fy"。这在图论中的意思是:如果 x 能管理 y,那么 y 一定在 x 的子树中。
2. 寻找主持人的逻辑
- 主持人必须能管理所有参与合作的员工,这意味着所有参与者都必须在主持人的子树内。
- 如果有多个满足条件的人,要选编号最大的。
3. 算法设计:预处理 + 暴力枚举剪枝
由于数据范围较小( N≤300,Q≤100N≤300,Q≤100 ),我们可以采用以下策略:
- 建树与求深度:记录每个人的直接领导和下属,并算出每个人在公司的"层级(深度)"。
- 寻找候选人的上限 :如果一个主持人能管理所有人,他的职位必须大于等于 所有人中最高的那个职位(即深度最浅的那个人)。因此,我们只需要检查深度 ≤≤ 参与者最小深度的那些节点即可。
- 倒序遍历找答案 :为了满足"编号最大"的要求,我们从 N−1N−1 到 00 倒着遍历节点。一旦找到第一个合法的候选人,直接输出并
break。 - DFS 验证管辖权:对于当前枚举到的候选人,通过一次 DFS 标记他子树里的所有人,然后核对参与者是否全被标记。
代码分析
cpp
#include<bits/stdc++.h>
using namespace std;
const int MAXN=310;
int fa[MAXN],dep[MAXN]; // fa[i]记录i的直接领导(父节点),dep[i]记录i的深度(层级)
bool vis[MAXN]; // DFS遍历时用来标记节点是否被访问过
vector<int> ch[MAXN]; // ch[i]记录i的所有直接下属(子节点)
解析:
- 定义了全局变量和常量。使用
vector数组ch来构建邻接表,存储树结构中每个节点的子节点列表。
cpp
int getDep(int x){ // 递归计算节点 x 的深度(根节点0的深度为0)
return x==0 ? 0 : getDep(fa[x])+1;
}
解析:
- 利用递归向上追溯父节点来计算深度。如果到了根节点
0,深度返回0;否则,其深度等于"直接领导的深度 + 1"。
cpp
void dfs(int x){ // DFS深度优先遍历:标记节点 x 以及 x 的所有下属(即 x 的整棵子树)
vis[x]=1;
for(int y:ch[x]) dfs(y); // for循环的简写方式(C++11特性),遍历x的所有子节点
}
解析:
- 标准的深度优先搜索。进入一个节点就打上
vis[x]=1的标记,然后对其所有的直接下属递归调用自身。执行完后,所有在x管辖范围内的员工都会被标记。
cpp
// 检查节点 x 是否能管理 vec 中的所有参与者
// 逻辑:如果 x 能管理所有人,那么 vec 里的每个人一定都在 x 的子树(管辖范围)里
bool check(int x,int n,const vector<int> &vec){
// 加上 const:既防止了函数内部误改数据,又配合 & 避免了数据拷贝,是更专业、更高效的写法。
for(int i=0;i<=n;i++) vis[i]=0; // 每次检查前,先把标记数组清空
dfs(x); // 标记 x 能管理的所有人
for(int y:vec){
if(vis[y]==0) return 0; // 遍历所有参与者,如果有任何一个人没被标记,说明 x 管不了他
}
return 1; // x 能管理所有人
}
解析:
- 这是一个封装好的验证函数。先重置
vis数组,再跑一遍dfs(x),最后逐一核对本次合作的人员名单。只要发现一个不在x子树内的人,立刻返回false(0)。
cpp
int main(){
ios::sync_with_stdio(false);
cin.tie(0); // 这两行用于关闭同步流,提高 cin/cout 的输入输出效率,防止超时
int n;
cin>>n; // 输入员工数量
for(int i=1;i<n;i++){
cin>>fa[i]; // 读入领导关系并建树
ch[fa[i]].push_back(i); // 把 i 加入到 fa[i] 的下属列表中
}
解析:
- 读取数据并完成建树操作。注意题目给的是 f1到 fn−1 ,所以循环从
i=1开始。
cpp
for(int i=1;i<n;i++) dep[i]=getDep(i); // 预处理计算出每个节点的深度
int q;
cin>>q; // 需要安排的合作场数
while(q--){
int m,minD=n+1; // minD用来记录当前组参与者中的最小深度(职位最高者的深度)
cin>>m; // 参与本次合作的员工数量
vector<int> vec(m); // 构建m大小的数组vec
for(int i=0;i<m;i++){
cin>>vec[i]; // 读入参与者
minD=min(minD,dep[vec[i]]); // 找出其中深度最浅的那个值(minD)
}
解析:
- 处理每一次查询。在读取参与者编号的同时,顺便求出这批人中最小的深度
minD。这是非常关键的剪枝 依据:任何深度大于minD的员工,绝对不可能成为这批人的共同管理者。
cpp
// 倒序枚举节点(从编号大到编号小),寻找符合要求的主持人
for(int i=n-1;i>=0;i--){
// 剪枝与判断:
// 1. dep[i] <= minD:领导 i 的深度必须小于等于参与者中最浅的深度(领导必须在最上面)
// 2. check(i, n, vec):领导 i 必须能管理 vec 中的所有人
if(dep[i]<=minD && check(i,n,vec)){
cout<<i<<"\n";
break; // 因为是倒序遍历,找到的第一个合法解一定是编号最大的,直接跳出
}
}
}
return 0; // 结束程序
}
解析:
- 核心查找逻辑。从
n-1递减到0。 dep[i] <= minD作为前置条件过滤掉了大量不可能的节点,极大地减少了check函数的调用次数。- 一旦
check返回true,立即输出该节点编号并break,完美契合题目"找编号最大的员工"的要求。