P1113 [USACO02FEB] 杂务

记录168

cpp 复制代码
#include<bits/stdc++.h>//万能头文件,包含了队列(queue)、向量(vector)等所有需要的库
using namespace std;

const int N=1e4+10;//题目中n<=10000,这里开10010防止数组越界
vector<int> G[N];//邻接表存图,G[i]存放从节点i出发能到达的所有后继节点
int inDegree[N];//入度数组,inDegree[i]表示指向节点i的边的数量(即i有几个前置任务没完成)
int len[N],dp[N];//len[i]表示完成杂务i所需的时间,dp[i]表示完成杂务i的最早完成时间
int n;//n表示需要完成的杂务总数 

void topoSort(){//拓扑排序+DP核心函数
	queue<int> q;//创建一个队列,用来存放当前入度为0(即前置任务全完成)的节点
	int ans=0;//记录全局最短完成时间(即所有杂务完成时间的最大值)
	
	for(int i=1;i<=n;i++){//遍历所有杂务
		if(inDegree[i]==0){//如果某个杂务没有前置任务(入度为0)
			q.push(i);//将其放入队列,作为拓扑排序的起点
			dp[i]=len[i];//核心:没有前置任务的杂务,它的最早完成时间就是它自身的耗时
		} 
	}
	
	while(!q.empty()){//只要队列里还有可以执行的杂务,就一直循环
		int u=q.front();//取出队首节点u(当前可以开始处理的杂务)
		q.pop();//将u移出队列
		
		ans=max(ans,dp[u]);//顺便更新全局最大值(因为可以并行,总耗时取决于最晚完成的那个杂务)
		
		for(int v:G[u]){//遍历u指向的所有后继节点v(即依赖u的后续杂务)
			dp[v]=max(dp[v],dp[u]+len[v]);//核心DP转移:v必须等所有前置任务中最慢的那个完成才能开始
			inDegree[v]--;//既然u已经处理完了,v的前置条件就少了一个,v的入度减1
			if(inDegree[v]==0){//如果v的入度减到了0,说明它的所有前置任务都完成了
				q.push(v);//此时v也可以开始执行了,将其放入队列
			}
		}
	}
	
	cout<<ans;//输出全局最大值,即完成所有杂务的最短时间
}

int main(){
	ios::sync_with_stdio(false);//输入输出优化,取消cin与scanf的同步
	cin.tie(0);//解除cin与cout的绑定,大幅提升输入输出速度
	
	cin>>n;//读入杂务总数n
	int x,y;//x是临时变量(当前杂务编号),y是临时变量(前置杂务编号)
	
	for(int i=1;i<=n;i++){//依次读入每个杂务的信息
		cin>>x>>len[x];//读入杂务编号x和它所需的耗时len[x](题目保证x是从1到n递增的)
		while(1){//使用死循环不断读入前置杂务,直到遇到0为止
			cin>>y;//读入一个前置杂务编号y
			if(y==0) break;//题目规定以0结束,读到0就跳出循环,处理下一个杂务
			
			G[y].push_back(x);//建边:从y指向x(必须先做y,才能做x)
			inDegree[x]++;//x被y指了,所以x的入度加1(x多了一个前置任务y)
		}
	}
	
	topoSort();//调用拓扑排序+DP函数,完成计算并输出结果
	return 0;//程序正常结束
}

题目传送门https://www.luogu.com.cn/problem/P1113


前言

我是一名专注信奥赛(GESP、CSP-J/S)的教练。

  • 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
  • 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

核心解题思路

  1. 问题转化(DAG 上的最长路)

    题目中的杂务有先后依赖关系,且保证无环(杂务 k 的准备工作只在 1 至 k-1 中),这构成了一个有向无环图(DAG)

    因为农场有足够多的工人,所有没有依赖关系的杂务可以并行 进行。所以,完成所有杂务的最短时间,取决于耗时最长的那条依赖链(即关键路径)。

  2. 算法设计(拓扑排序 + DP)

    • 状态定义dp[i] 表示完成杂务 i 的最早完成时间
    • 状态转移 :对于一条依赖边 u -> v(u 是 v 的前置任务),v 必须等 u 完成后才能开始。所以 dp[v] = max(dp[v], dp[u] + len[v])
    • 拓扑排序的作用 :保证在处理节点 v 时,所有能到达 v 的前置节点 u 的 dp[u] 都已经计算完毕。当 v 的入度变为 0 时,说明所有前置任务都已完成,此时 dp[v] 就是最终的最早完成时间。

代码分块详细解释

1. 头文件、全局变量与数组定义

cpp 复制代码
#include<bits/stdc++.h> // 万能头文件
using namespace std;

const int N = 1e4 + 10; // 题目 n<=10000,开 10010 防止越界
vector<int> G[N]; // 邻接表:G[u] 存储 u 的所有后继任务
int inDegree[N]; // 入度数组:记录任务 i 有几个前置任务
int len[N], dp[N]; // len[i]: 任务 i 耗时; dp[i]: 任务 i 的最早完成时间
int n; // 杂务总数
  • 详细分析
    • 邻接表 G[N]:存储有向边,方向是从前置任务指向后续任务。
    • 入度 inDegree:拓扑排序的核心,入度为 0 表示所有前置任务都已完成。
    • DP 数组dp[i] 初始为 0,后续通过转移更新。

2. 核心逻辑:拓扑排序 + DP

cpp 复制代码
void topoSort(){
    queue<int> q; // 队列:存放当前入度为 0 的任务
    int ans = 0; // 记录全局最短完成时间(所有任务完成时间的最大值)
    
    // 1. 初始化:将所有入度为 0 的任务入队
    for(int i = 1; i <= n; i++){
        if(inDegree[i] == 0){
            q.push(i);
            dp[i] = len[i]; // 核心:没有前置任务,最早完成时间就是自身耗时
        } 
    }
    
    // 2. 开始拓扑排序
    while(!q.empty()){
        int u = q.front(); // 取出队首任务 u
        q.pop();
        
        ans = max(ans, dp[u]); // 更新全局最大值(并行任务,总时间取决于最慢的)
        
        // 3. 遍历 u 的所有后继任务 v
        for(int v : G[u]){
            // 核心 DP 转移:v 必须等所有前置中最慢的完成才能开始
            dp[v] = max(dp[v], dp[u] + len[v]);
            
            inDegree[v]--; // u 完成了,v 的前置条件少了一个
            
            // 4. 如果 v 的所有前置都完成了,v 可以开始执行
            if(inDegree[v] == 0) q.push(v); 
        } 
    } 
    
    cout << ans; // 输出全局最大值
}
  • 详细分析
    • DP 转移时机dp[v] = max(dp[v], dp[u] + len[v]) 在入度减 1 之前执行。这意味着每处理一条边,就尝试更新一次 v 的最早完成时间。
    • 入队时机 :只有当 inDegree[v] == 0 时才入队。这保证了当 v 被取出处理它的后继时,dp[v] 已经是所有前驱转移过来的最大值。
    • 全局最大值 :因为任务是并行的,所有任务都完成的时间,就是所有 dp[i] 中的最大值。

3. 主函数:建图与初始化

cpp 复制代码
int main(){
    ios::sync_with_stdio(false); // IO 优化
    cin.tie(0);
     
    cin >> n;
    int x, y;
    
    // 读入 n 个杂务的信息
    for(int i = 1; i <= n; i++){
        cin >> x >> len[x]; // 读入杂务编号 x 和耗时 len[x]
        while(1){
            cin >> y; // 读入一个前置杂务编号 y
            if(y == 0) break; // 读到 0 结束当前行
            
            G[y].push_back(x); // 建边:y -> x(y 是 x 的前置)
            inDegree[x]++; // x 的入度加 1
        }
    }
    
    topoSort(); // 执行拓扑排序 + DP
    return 0;
}
  • 详细分析
    • 建边方向 :题目说"y 是 x 的准备工作",所以边是 y -> x
    • 输入处理 :每行以 0 结尾,使用 while(1) + break 处理。
    • 题目保证:杂务编号从 1 到 n 递增,所以读入顺序就是拓扑序的一种可能,但代码依然用拓扑排序保证正确性。

核心逻辑总结表

代码模块 核心变量/操作 精炼作用 解决的痛点
邻接表存图 vector<int> G[N] 存储有向边 y -> x 灵活处理稀疏图,节省空间
入度统计 inDegree[x]++ 记录任务的前置数量 拓扑排序的判断依据
DP 初始化 dp[i] = len[i] 设置无前置任务的完成时间 保证起点正确
状态转移 dp[v] = max(dp[v], dp[u]+len[v]) 利用前驱更新后继的最早完成时间 实现 DAG 上的最长路 DP
入队控制 if(inDegree[v] == 0) 确保前置全部完成再入队 保证 DP 转移的完备性
全局最大值 ans = max(ans, dp[u]) 记录所有任务完成时间的最大值 解决并行任务的总耗时问题

易错点提示

  1. 建边方向搞反

    • 题目说"y 是 x 的准备工作",所以必须建边 y -> x
    • 如果建成 x -> y,拓扑排序会从后续任务往前置任务跑,结果完全错误。
  2. DP 初始化错误

    • 入度为 0 的任务,dp[i] 必须初始化为 len[i]
    • 如果初始化为 0,那么 dp[u] + len[v] 会少算 u 自身的耗时,导致结果偏小。
  3. 全局最大值遗漏

    • 最终答案不是 dp[n],而是所有 dp[i] 的最大值。
    • 因为任务是并行的,最后完成的任务不一定是编号最大的那个。
  4. 输入格式处理

    • 每行以 0 结尾,必须用 while(1) + break 处理。
    • 如果直接用 cin >> y 然后判断,可能会漏读或错读。
  5. 多组数据未清空

    • 本题是单组数据,但如果变成多组,必须清空 GinDegreedplen
    • vector 清空:G[i].clear()
    • 数组清空:memset(inDegree, 0, sizeof(inDegree))
相关推荐
月光船幽幽1 小时前
门控函数SHS阈值与调制机制解析
人工智能·python·算法
此生决int1 小时前
深入理解C++系列(11)——stack、queue和priority_queue
开发语言·c++
Doraemomo1 小时前
数据结构-哈希表
数据结构·算法·散列表
君君思密达1 小时前
Leetcode Hot 100 题目详解-哈希表
数据结构·算法·leetcode
lucas_AI1 小时前
Muse Glimmer 30B:Meta 难得给的真开源,强在哪、虚在哪
人工智能·算法
ai产品老杨1 小时前
国产NPU视觉算法完整流程
算法
过期的秋刀鱼!1 小时前
带替换的采样
人工智能·python·算法·决策树·机器学习
过期的秋刀鱼!2 小时前
使用多个决策树
人工智能·算法·决策树·机器学习·数据挖掘
小徐不徐说2 小时前
VS 2022 Visual Assist 破解
c++·visualstudio·工具·插件