记录173
#include<bits/stdc++.h>//万能头文件
using namespace std;
const int N=1010;//题目n,m<=1000,开1010防越界
vector<int> G[N];//邻接表存图,G[i]存放所有级别比i高的火车站
int inDegree[N];//入度数组,记录有多少个火车站的级别比当前火车站低
int dp[N];//dp[i]表示火车站i的最低级别
bool hasEdge[N][N];//【新增】去重数组,hasEdge[u][v]为true表示u到v的边已经建过
int n,m;//n是火车站数量,m是车次数量
void topoSort(){//拓扑排序+DP核心函数
queue<int> q;//创建队列,存放当前入度为0的火车站
//把所有入度为0的火车站入队(即没有比它级别更低的站,初始级别为1)
for(int i=1;i<=n;i++){
if(inDegree[i]==0){
q.push(i);
dp[i]=1;//初始级别为1
}
}
while(!q.empty()){//只要队列里还有可以处理的火车站,就一直循环
int u=q.front();//取出队首火车站u
q.pop();
//遍历u的所有后继节点(级别比u高的火车站)
for(int v:G[u]){
//核心DP转移:v的级别至少是u的级别+1
dp[v]=max(dp[v],dp[u]+1);
//拓扑排序常规操作:入度减1,为0则入队
inDegree[v]--;
if(inDegree[v]==0) q.push(v);
}
}
//找出所有火车站中的最高级别,就是答案
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
cout<<ans;
}
int main(){
ios::sync_with_stdio(false);//输入输出优化
cin.tie(0);
cin>>n>>m;//读入火车站数量和车次数量
for(int i=1;i<=m;i++){
int s;//当前车次的停靠站数量
cin>>s;
vector<int> stop(s+1);//记录当前车次的停靠站
for(int j=1;j<=s;j++) cin>>stop[j];
//标记当前车次的停靠站
bool isStop[N]={0};
for(int j=1;j<=s;j++) isStop[stop[j]]=true;
//根据规则建边:未停靠站 -> 停靠站
int start=stop[1],end=stop[s];
for(int station=start;station<=end;station++){
if(!isStop[station]){//如果station是未停靠站
for(int j=1;j<=s;j++){
int target=stop[j];//target是停靠站
//【核心修复】如果这条边已经建过,直接跳过,防止重复建边导致MLE
if(hasEdge[station][target]) continue;
//建边并标记
hasEdge[station][target]=true;
G[station].push_back(target);
inDegree[target]++;
}
}
}
}
topoSort();//调用拓扑排序+DP函数,完成计算并输出结果
return 0;//程序正常结束
}
题目传送门
https://www.luogu.com.cn/problem/P1983
前言
我是一名专注信奥赛(GESP、CSP-J/S)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
-
问题转化(偏序关系与图论模型):
题目规定:“如果车次停靠了 x,则始发站到终点站之间所有级别 >= x 的站都必须停靠”。
换个角度理解:如果某个站 y 没有被停靠,那么 y 的级别一定严格小于这趟车停靠的所有站。
这就构成了一个有向无环图(DAG)中的偏序关系:我们可以从每一个“未停靠站”向这趟车的所有“停靠站”连一条有向边。这代表“未停靠站的级别 < 停靠站的级别”。 -
算法设计(拓扑排序 + DP 求最长路):
- 状态定义:
dp[i]表示火车站i的最低级别。 - 初始化:入度为 0 的节点(没有任何站比它级别低),级别为 1。
- 状态转移:对于一条边
u -> v(u 级别 < v 级别),v的级别至少是u的级别 + 1,即dp[v] = max(dp[v], dp[u] + 1)。 - 最终答案:所有火车站级别的最大值。
- 状态定义:
代码分块详细解释
1. 头文件、全局变量与去重数组
#include<bits/stdc++.h> // 万能头文件
using namespace std;
const int N = 1010; // 题目 n,m<=1000,开 1010 防越界
vector<int> G[N]; // 邻接表:G[u] 存放所有级别比 u 高的站
int inDegree[N]; // 入度数组:记录有多少个站的级别比当前站低
int dp[N]; // DP数组:dp[i] 表示火车站 i 的最低级别
bool hasEdge[N][N]; // 【关键】去重数组,防止重复建边导致超时或内存爆炸
int n, m; // n 是火车站数量,m 是车次数量
- 详细分析:
- 去重数组
hasEdge:这是本题最大的坑点!如果两趟车都经过了站 A 和站 B,不加判断直接建边,会导致同一条边被建立多次,拓扑排序时入度会错误地增加多次,最终结果完全错误。
- 去重数组
2. 核心逻辑:拓扑排序 + DP 求最长路
void topoSort(){
queue<int> q; // 队列:存放当前入度为 0 的节点
// 1. 初始化:将所有入度为 0 的站入队,级别设为 1
for(int i = 1; i <= n; i++){
if(inDegree[i] == 0){
q.push(i);
dp[i] = 1; // 最低级别为 1
}
}
// 2. 开始拓扑排序
while(!q.empty()){
int u = q.front(); // 取出队首节点 u
q.pop();
// 3. 遍历 u 的所有后继节点 v(级别比 u 高的站)
for(int v : G[u]){
// 核心 DP 转移:v 的级别至少是 u 的级别 + 1
dp[v] = max(dp[v], dp[u] + 1);
inDegree[v]--; // u 处理完了,v 的前置条件少了一个
if(inDegree[v] == 0) q.push(v); // 如果 v 的所有前置都处理完了,v 入队
}
}
// 4. 找出所有火车站中的最高级别,就是答案
int ans = 0;
for(int i = 1; i <= n; i++) ans = max(ans, dp[i]);
cout << ans;
}
- 详细分析:
- DP 转移时机:
dp[v] = max(dp[v], dp[u] + 1)必须在入度减 1 之前执行,确保每一条边都被用来尝试更新最大值。 - 入队时机:只有当
inDegree[v] == 0时才入队,保证dp[v]已经考虑了所有前驱节点的贡献。
- DP 转移时机:
3. 主函数:建图与去重逻辑
int main(){
ios::sync_with_stdio(false); // IO 优化
cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int s; // 当前车次的停靠站数量
cin >> s;
vector<int> stop(s + 1); // 记录当前车次的停靠站
for(int j = 1; j <= s; j++) cin >> stop[j];
// 标记当前车次的停靠站,方便 O(1) 查询
bool isStop[N] = {0};
for(int j = 1; j <= s; j++) isStop[stop[j]] = true;
// 根据规则建边:未停靠站 -> 停靠站
int start = stop[1], end = stop[s];
for(int station = start; station <= end; station++){
if(!isStop[station]){ // 如果 station 是未停靠站
for(int j = 1; j <= s; j++){
int target = stop[j]; // target 是停靠站
// 【核心修复】如果这条边已经建过,直接跳过
if(hasEdge[station][target]) continue;
// 建边并标记
hasEdge[station][target] = true;
G[station].push_back(target);
inDegree[target]++;
}
}
}
}
topoSort(); // 执行拓扑排序 + DP
return 0;
}
- 详细分析:
- 建边方向:从“未停靠站”指向“停靠站”,代表级别低 -> 级别高。
- 去重逻辑:
if(hasEdge[station][target]) continue;是本题的灵魂,没有它,数据稍微大一点就会 MLE 或 TLE。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 去重数组 | bool hasEdge[N][N] | 防止同一条边被建立多次 | 避免入度错误累加,防止 MLE/TLE |
| 邻接表存图 | vector<int> G[N] | 存储有向边 u -> v | 灵活处理稀疏图,节省空间 |
| DP 初始化 | dp[i] = 1 (当 inDegree==0) | 设置最低级别的基础值 | 保证起点正确 |
| 状态转移 | dp[v] = max(dp[v], dp[u]+1) | 利用前驱更新后继的最低级别 | 实现 DAG 上的最长路 DP |
| 入队控制 | if(inDegree[v] == 0) | 确保前置全部处理完再入队 | 保证 DP 转移的完备性 |
| 全局最大值 | ans = max(ans, dp[i]) | 记录所有站中的最高级别 | 满足题目“至少分为几个级别”的要求 |
易错点提示
-
忘记去重(最致命):
- 必须使用
hasEdge数组判断边是否已经存在。 - 如果不去重,同一条边被建两次,
inDegree会多加 1,导致拓扑排序永远无法把该节点入队,最终dp值错误。
- 必须使用
-
建边方向搞反:
- 题目隐含“未停靠站级别 < 停靠站级别”,所以边必须是
未停靠站 -> 停靠站。 - 如果建反了,拓扑排序会从高级别往低级别跑,结果完全错误。
- 题目隐含“未停靠站级别 < 停靠站级别”,所以边必须是
-
DP 初始化错误:
- 入度为 0 的节点,
dp[i]必须初始化为 1。 - 如果初始化为 0,那么
dp[u] + 1会少算 1,最终结果偏小。
- 入度为 0 的节点,
-
答案统计错误:
- 最终答案不是
dp[n],而是所有dp[i]的最大值。 - 因为最高级别的站不一定是编号最大的那个。
- 最终答案不是
-
多组数据未清空:
- 本题是单组数据,但如果变成多组,必须清空
G、inDegree、dp、hasEdge。 vector清空:G[i].clear()。- 数组清空:
memset(hasEdge, 0, sizeof(hasEdge))。
- 本题是单组数据,但如果变成多组,必须清空

1418

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



