P1700 [USACO19OPEN] Milk Factory B

记录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代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

核心解题思路

这道题是一道典型的图论与暴力枚举结合的问题。

  1. 问题转化
    题目给出了一个包含 NN 个结点和 N−1N−1 条有向边的图(本质是一棵有向树)。我们需要找到一个结点 ii ,使得图中所有其他结点都能通过有向边到达 ii 。如果存在多个这样的结点,输出编号最小的那个;如果不存在,输出 -1。

  2. 算法设计(暴力枚举 + 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 的小数据范围,用简单直观的暴力法替代复杂的图论算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值