记录137
#include<bits/stdc++.h>
using namespace std;
const int N=105;// 定义常量N,表示加工站数量的最大范围(题目保证N<=100)
vector<int> g[N];// 定义邻接表g,g[u]存储从结点u出发能直接到达的所有结点
bool vis[N];// 定义访问标记数组,用于DFS时防止重复访问
bool found_target=0; // 定义标志变量,用于在DFS中记录是否成功找到了目标点target
// 深度优先搜索(DFS)函数,从当前结点u出发,寻找目标点target
void dfs(int u,int target){
if(found_target) return; // 如果之前已经找到了目标点,直接剪枝返回
if(u==target){ // 如果当前结点u就是目标点target
found_target=1; // 标记已找到目标点
return; // 直接返回
}
vis[u]=1; // 标记当前结点u已被访问过
for(int v:g[u]){ // 遍历当前结点u能到达的所有相邻结点v
if(!vis[v]) dfs(v,target);
}// 如果结点v还没有被访问过,继续从v出发进行深搜
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;// 定义变量n,表示加工站的数量
cin>>n; // 读入加工站数量n
int a,b; // 定义变量a和b,用来临时存储每条传送带的起点和终点
for(int i=1;i<n;i++){ //n-1条边
cin>>a>>b; // 读入一条从a指向b的传送带
g[a].push_back(b); // 将b加入a的邻接表(表示a能走到b)
}
// 从小到大枚举每一个结点i作为终点
for(int i=1;i<=n;i++){
bool is_ok=1; // 假设当前目标是合法的
// 检查所有其他结点j,看是否都能通过DFS走到目标i
for(int j=1;j<=n;j++){
if(j==i) continue; // 如果j就是目标本身,不需要检查
memset(vis,0,sizeof(vis)); //走之前重置访问数组
found_target=0; // 一开始没找到目标
dfs(j,i); // 从结点j出发,尝试寻找目标i
if(!found_target){ // 如果从j出发无法走到目标i
is_ok=0; // 当前目标不合法
break; //提前跳出内层循环,检查下一个target
}
}
if(is_ok){ // 如果当前i满足所有结点都能到达它
cout<<i<<"\n"; // 输出最小的满足条件的i
return 0; // 程序直接结束
}
}
cout<<-1; // 如果遍历完所有target都不合法,输出-1
return 0;//程序正常结束
}
题目传送门
https://www.luogu.com.cn/problem/P1700
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题是一道典型的图论与暴力枚举结合的问题。
-
问题转化:
题目给出了一个包含 NN 个结点和 N−1N−1 条有向边的图(本质是一棵有向树)。我们需要找到一个结点 ii ,使得图中所有其他结点都能通过有向边到达 ii 。如果存在多个这样的结点,输出编号最小的那个;如果不存在,输出 -1。 -
算法设计(暴力枚举 + DFS):
由于题目中 NN 的最大值仅为 100,数据规模非常小。我们可以采用最直观的暴力枚举法:- 依次假设每一个结点 ii (从 1 到 NN )是我们要找的“终点”。
- 对于每一个假设的终点 ii ,我们从图中的每一个其他结点 jj 出发,使用深度优先搜索(DFS)或广度优先搜索(BFS),判断是否能沿着有向边到达 ii 。
- 如果所有结点 jj 都能到达 ii ,那么 ii 就是合法的答案。因为我们是从 1 开始从小到大枚举的,所以找到的第一个合法 ii 必然是最小的。
代码分块详细解释
1. 头文件、常量与全局变量定义
#include<bits/stdc++.h>
using namespace std;
const int N=105; // 定义常量 N,表示加工站数量的最大范围(题目保证 N<=100)
vector<int> g[N]; // 定义邻接表 g,g[u] 存储从结点 u 出发能直接到达的所有结点
bool vis[N]; // 定义访问标记数组,用于 DFS 时防止重复访问
bool found_target=0; // 定义标志变量,用于在 DFS 中记录是否成功找到了目标点 target
- 详细分析:这部分搭建了图论算法的基础。由于是有向图,我们使用
vector<int> g[N]来构建邻接表,g[a].push_back(b)表示存在一条从 aa 指向 bb 的单向传送带。vis数组是 DFS 的标配,防止在图中走环路导致死循环。found_target是一个全局剪枝变量,一旦在某次 DFS 中找到了目标,就立刻停止后续的搜索,提高效率。
2. 深度优先搜索(DFS)函数
// 深度优先搜索(DFS)函数,从当前结点 u 出发,寻找目标点 target
void dfs(int u, int target){
if(found_target) return; // 如果之前已经找到了目标点,直接剪枝返回
if(u == target){ // 如果当前结点 u 就是目标点 target
found_target = 1; // 标记已找到目标点
return; // 直接返回
}
vis[u] = 1; // 标记当前结点 u 已被访问过
for(int v : g[u]){ // 遍历当前结点 u 能到达的所有相邻结点 v
if(!vis[v]) dfs(v, target);
} // 如果结点 v 还没有被访问过,继续从 v 出发进行深搜
}
- 详细分析:这是判断“点 jj 能否到达点 ii ”的核心工具。
- 剪枝优化:函数开头首先检查
found_target,如果之前的搜索路径已经找到了目标,当前递归分支就没有继续的必要了,直接返回。 - 命中目标:如果当前遍历到的结点
u等于目标target,说明路径打通,将全局标志置为 1 并返回。 - 递归扩展:如果没有找到目标,就标记当前点已访问,然后遍历它的所有出边。只有当相邻结点
v未被访问时,才递归进入,这保证了搜索的有限性。
- 剪枝优化:函数开头首先检查
3. 主函数:建图与输入处理
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n; // 定义变量 n,表示加工站的数量
cin >> n; // 读入加工站数量 n
int a, b; // 定义变量 a 和 b,用来临时存储每条传送带的起点和终点
for(int i = 1; i < n; i++){ // n-1 条边
cin >> a >> b; // 读入一条从 a 指向 b 的传送带
g[a].push_back(b); // 将 b 加入 a 的邻接表(表示 a 能走到 b)
}
- 详细分析:标准的 IO 加速和建图过程。题目给出了 N−1N−1 条有向边,我们按照输入顺序,将每条边的终点加入起点的邻接表中,从而构建出完整的有向图。
4. 核心逻辑:枚举终点与合法性检查
// 从小到大枚举每一个结点 i 作为终点
for(int i = 1; i <= n; i++){
bool is_ok = 1; // 假设当前目标是合法的
// 检查所有其他结点 j,看是否都能通过 DFS 走到目标 i
for(int j = 1; j <= n; j++){
if(j == i) continue; // 如果 j 就是目标本身,不需要检查
memset(vis, 0, sizeof(vis)); // 走之前重置访问数组
found_target = 0; // 一开始没找到目标
dfs(j, i); // 从结点 j 出发,尝试寻找目标 i
if(!found_target){ // 如果从 j 出发无法走到目标 i
is_ok = 0; // 当前目标不合法
break; // 提前跳出内层循环,检查下一个 target
}
}
if(is_ok){ // 如果当前 i 满足所有结点都能到达它
cout << i << "\n"; // 输出最小的满足条件的 i
return 0; // 程序直接结束
}
}
cout << -1; // 如果遍历完所有 target 都不合法,输出 -1
return 0; // 程序正常结束
}
- 详细分析:这是算法的主体部分,采用了双重循环。
- 外层循环(枚举假设):我们依次假设结点 ii 是那个“所有点都能到达的终点”。因为题目要求输出最小的 ii ,所以我们从 1 开始从小到大枚举,一旦找到合法的就可以直接输出并结束程序。
- 内层循环(验证假设):对于当前的假设终点 ii ,我们需要验证图中的每一个其他结点 jj 是否都能到达它。
- 状态重置:每次从一个新的起点 jj 开始 DFS 之前,必须将访问数组
vis清零,并将found_target重置为 0。这是因为每次 DFS 都是一次全新的探索。 - 提前终止:如果在验证过程中,发现有任何一个结点 jj 无法到达 ii (即
!found_target),那么当前的 ii 肯定不是答案,直接break跳出内层循环,去测试下一个 ii 。这极大地减少了不必要的计算。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 数据结构 | vector<int> g[N] | 使用邻接表存储有向图 | 高效存储单向传送带,方便通过起点快速找到所有能直接到达的终点 |
| 路径搜索 | dfs(u, target) | 判断从 u 到 target 是否有通路 | 解决有向图中两点连通性的判断问题 |
| 防死循环 | vis[u]=1 与 if(!vis[v]) | 记录已访问的结点 | 防止在 DFS 过程中因为图中存在环路或重复路径而导致无限递归 |
| 全局剪枝 | found_target | 记录目标是否已被找到 | 一旦在 DFS 的某条分支中找到了目标,立即停止所有后续搜索,提升效率 |
| 暴力枚举 | 双重 for 循环 | 依次假设每个点为终点并验证 | 利用 N≤100N≤100 的小数据范围,用简单直观的暴力法替代复杂的图论算法 |
679

被折叠的 条评论
为什么被折叠?



