P1983 [NOIP 2013 普及组] 车站分级

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

核心解题思路

  1. 问题转化(偏序关系与图论模型)
    题目规定:“如果车次停靠了 x,则始发站到终点站之间所有级别 >= x 的站都必须停靠”。
    换个角度理解:如果某个站 y 没有被停靠,那么 y 的级别一定严格小于这趟车停靠的所有站
    这就构成了一个有向无环图(DAG)中的偏序关系:我们可以从每一个“未停靠站”向这趟车的所有“停靠站”连一条有向边。这代表“未停靠站的级别 < 停靠站的级别”。

  2. 算法设计(拓扑排序 + 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] 已经考虑了所有前驱节点的贡献。

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])记录所有站中的最高级别满足题目“至少分为几个级别”的要求

易错点提示

  1. 忘记去重(最致命)

    • 必须使用 hasEdge 数组判断边是否已经存在。
    • 如果不去重,同一条边被建两次,inDegree 会多加 1,导致拓扑排序永远无法把该节点入队,最终 dp 值错误。
  2. 建边方向搞反

    • 题目隐含“未停靠站级别 < 停靠站级别”,所以边必须是 未停靠站 -> 停靠站
    • 如果建反了,拓扑排序会从高级别往低级别跑,结果完全错误。
  3. DP 初始化错误

    • 入度为 0 的节点,dp[i] 必须初始化为 1。
    • 如果初始化为 0,那么 dp[u] + 1 会少算 1,最终结果偏小。
  4. 答案统计错误

    • 最终答案不是 dp[n],而是所有 dp[i] 的最大值。
    • 因为最高级别的站不一定是编号最大的那个。
  5. 多组数据未清空

    • 本题是单组数据,但如果变成多组,必须清空 GinDegreedphasEdge
    • vector 清空:G[i].clear()
    • 数组清空:memset(hasEdge, 0, sizeof(hasEdge))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值