题解:AcWing 456 车站分级
【题目来源】
AcWing:456. 车站分级 - AcWing题库
【题目描述】
一条单向的铁路线上,依次有编号为 \(1, 2, …, n\) 的 \(n\) 个火车站。
每个火车站都有一个级别,最低为 \(1\) 级。
现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次停靠了火车站 \(x\),则始发站、终点站之间所有级别大于等于火车站 \(x\) 的都必须停靠。(注意:起始站和终点站自然也算作事先已知需要停靠的站点)
例如,下表是 \(5\) 趟车次的运行情况。
其中,前 \(4\) 趟车次均满足要求,而第 \(5\) 趟车次由于停靠了 \(3\) 号火车站(\(2\) 级)却未停靠途经的 \(6\) 号火车站(亦为 \(2\) 级)而不满足要求。

现有 \(m\) 趟车次的运行情况(全部满足要求),试推算这 \(n\) 个火车站至少分为几个不同的级别。
【输入】
第一行包含 \(2\) 个正整数 \(n,m\),用一个空格隔开。
第 \(i+1\) 行(\(1≤i≤m\))中,首先是一个正整数 \(s_i\)(\(2≤s_i≤n\)),表示第 \(i\) 趟车次有 \(s_i\) 个停靠站;接下来有 \(s_i\) 个正整数,表示所有停靠站的编号,从小到大排列。
每两个数之间用一个空格隔开。输入保证所有的车次都满足要求。
【输出】
输出只有一行,包含一个正整数,即 \(n\) 个火车站最少划分的级别数。
【输入样例】
9 3
4 1 3 5 6
3 3 5 6
3 1 5 9
【输出样例】
3
【核心思想】
-
问题分析:给定 \(n\) 个车站和 \(m\) 趟车次的停靠情况,每趟车次满足:如果停靠了车站 \(x\),则区间内的所有级别大于等于 \(x\) 的车站都必须停靠。需要求最少需要几个不同的级别。这是一个拓扑排序求最长路问题,关键在于根据停靠关系建立级别大小的约束图。
-
算法选择:
- 约束分析:对于每趟车次,在途经区间 \([start, end]\) 内,停靠的站级别高,没停靠的站级别低
- 建图优化:使用虚拟节点减少边数
- 虚拟节点 \(vir = n + i\) 代表第 \(i\) 趟车次
- 没停靠的站 \(j \rightarrow vir\)(权值 \(0\)):表示 \(j\) 的级别 \(\leq\) 车次代表的级别
- \(vir \rightarrow\) 停靠的站 \(j\)(权值 \(1\)):表示车次代表的级别 \(< j\) 的级别
- 拓扑排序:求最长路,最长链的长度即为最少级别数
-
关键步骤:
- 读取输入:\(n\)(车站数)、\(m\)(车次数)
- 初始化:邻接表、入度数组、层级数组
- 处理每个车次(\(i\) 从 \(1\) 到 \(m\)):
- 读取停靠站,标记
st[stop] = true - 确定途经区间 \([start, end]\)
- 创建虚拟节点 \(vir = n + i\)
- 遍历区间 \([start, end]\):
- 若 \(j\) 没停靠:
add(j, vir, 0) - 若 \(j\) 停靠:
add(vir, j, 1)
- 若 \(j\) 没停靠:
- 读取停靠站,标记
- 拓扑排序求最长路:
- 初始化所有节点层级为 \(1\)
- 入度为 \(0\) 的节点入队
- BFS:更新邻接节点层级
lvl[j] = max(lvl[j], lvl[u] + w)
- 计算答案:在所有原站点中取最大层级
- 输出结果:最少级别数 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m + \sum s_i)\),其中 \(s_i\) 是第 \(i\) 趟车次的站点数
- 空间复杂度:\(O(n + m + \sum s_i)\),存储图结构
-
拓扑排序求最长路的核心思想:
- 约束建图:将级别大小关系转化为有向边,级别低的指向级别高的
- 虚拟节点优化:避免没停靠站向所有停靠站连边,通过虚拟节点将边数从 \(O(s_i^2)\) 降为 \(O(s_i)\)
- 最长路性质:DAG 中最长链的长度即为最少需要的级别数(可以类比染色问题)
- 拓扑排序:利用 DAG 的拓扑序,动态规划求最长路
- 适用于约束满足、层级划分、最小染色等问题
【算法标签】
拓扑排序
【代码详解】
#include <bits/stdc++.h>
using namespace std;
// ================= 常量与全局变量 =================
const int N = 2005; // 最大节点数(原站点 + 虚拟节点)
const int M = 1000005; // 最大边数
int n; // 站点数量(原图节点数)
int m; // 车次数量
int ans; // 最终答案
int h[N]; // 邻接表头结点
int e[M]; // 边的终点
int ne[M]; // 下一条边的索引
int w[M]; // 边的权值
int idx; // 边的计数器
int lvl[N]; // 每个节点的层级(拓扑排序后得到的最长路距离)
int ind[N]; // 每个节点的入度
int dist[N]; // 未使用的数组(保留原样)
bool st[N]; // 标记站点是否在当前车次中停靠
queue<int> q; // 拓扑排序队列
// ================= 添加有向边 =================
void add(int a, int b, int c)
{
e[idx] = b; // 边的终点
ne[idx] = h[a]; // 头插法
w[idx] = c; // 边的权值
h[a] = idx++; // 更新头结点
ind[b]++; // 终点入度加 1
}
// ================= 拓扑排序求最长路 =================
void topsort()
{
// 初始化所有站点的层级为 1
for (int i = 1; i <= n; i++)
lvl[i] = 1;
// 将所有入度为 0 的节点入队(包括原站点和虚拟节点)
for (int i = 1; i <= n + m; i++)
if (ind[i] == 0)
q.push(i);
// BFS 拓扑排序
while (!q.empty())
{
int u = q.front();
q.pop();
// 遍历 u 的所有邻接边
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接节点
lvl[j] = max(lvl[j], lvl[u] + w[i]); // 更新最长路距离
if (--ind[j] == 0) // 入度减为 0 时入队
q.push(j);
}
}
}
// ================= 主函数 =================
int main()
{
// 读取站点数和车次数
cin >> n >> m;
// 初始化邻接表头结点为 -1
memset(h, -1, sizeof(h));
// 处理每个车次
for (int i = 1; i <= m; i++)
{
// 重置标记数组
memset(st, 0, sizeof(st));
int cnt; // 当前车次停靠的站点数
cin >> cnt;
int start = n; // 当前车次途经的最小站点编号
int end = 1; // 当前车次途经的最大站点编号
// 读取当前车次停靠的所有站点
while (cnt--)
{
int stop;
cin >> stop;
start = min(start, stop); // 更新最小站点编号
end = max(end, stop); // 更新最大站点编号
st[stop] = true; // 标记该站点停靠
}
// 创建虚拟节点 vir = n + i
int vir = n + i;
// 在 [start, end] 范围内建边
for (int j = start; j <= end; j++)
{
if (!st[j]) // 不停靠的站点
add(j, vir, 0); // 从站点指向虚拟节点,权值为 0
else // 停靠的站点
add(vir, j, 1); // 从虚拟节点指向站点,权值为 1
}
}
// 执行拓扑排序求最长路
topsort();
// 在所有原站点中取最大层级作为答案
for (int i = 1; i <= n; i++)
ans = max(ans, lvl[i]);
// 输出结果
cout << ans << endl;
return 0;
}
【运行结果】
9 3
4 1 3 5 6
3 3 5 6
3 1 5 9
3
浙公网安备 33010602011771号