记录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代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
-
问题转化(DAG 上的最长路) :
题目中的杂务有先后依赖关系,且保证无环(杂务 k 的准备工作只在 1 至 k-1 中),这构成了一个有向无环图(DAG) 。
因为农场有足够多的工人,所有没有依赖关系的杂务可以并行 进行。所以,完成所有杂务的最短时间,取决于耗时最长的那条依赖链(即关键路径)。
-
算法设计(拓扑排序 + 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]中的最大值。
- DP 转移时机 :
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 递增,所以读入顺序就是拓扑序的一种可能,但代码依然用拓扑排序保证正确性。
- 建边方向 :题目说"y 是 x 的准备工作",所以边是
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 邻接表存图 | 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]) |
记录所有任务完成时间的最大值 | 解决并行任务的总耗时问题 |
易错点提示
-
建边方向搞反:
- 题目说"y 是 x 的准备工作",所以必须建边
y -> x。 - 如果建成
x -> y,拓扑排序会从后续任务往前置任务跑,结果完全错误。
- 题目说"y 是 x 的准备工作",所以必须建边
-
DP 初始化错误:
- 入度为 0 的任务,
dp[i]必须初始化为len[i]。 - 如果初始化为 0,那么
dp[u] + len[v]会少算 u 自身的耗时,导致结果偏小。
- 入度为 0 的任务,
-
全局最大值遗漏:
- 最终答案不是
dp[n],而是所有dp[i]的最大值。 - 因为任务是并行的,最后完成的任务不一定是编号最大的那个。
- 最终答案不是
-
输入格式处理:
- 每行以 0 结尾,必须用
while(1)+break处理。 - 如果直接用
cin >> y然后判断,可能会漏读或错读。
- 每行以 0 结尾,必须用
-
多组数据未清空:
- 本题是单组数据,但如果变成多组,必须清空
G、inDegree、dp、len。 vector清空:G[i].clear()。- 数组清空:
memset(inDegree, 0, sizeof(inDegree))。
- 本题是单组数据,但如果变成多组,必须清空