题解:AcWing 456 车站分级

【题目来源】

AcWing:456. 车站分级 - AcWing题库

【题目描述】

一条单向的铁路线上,依次有编号为 \(1, 2, …, n\) 的 \(n\) 个火车站。

每个火车站都有一个级别,最低为 \(1\) 级。

现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次停靠了火车站 \(x\),则始发站、终点站之间所有级别大于等于火车站 \(x\) 的都必须停靠。(注意:起始站和终点站自然也算作事先已知需要停靠的站点)

例如,下表是 \(5\) 趟车次的运行情况。

其中,前 \(4\) 趟车次均满足要求,而第 \(5\) 趟车次由于停靠了 \(3\) 号火车站(\(2\) 级)却未停靠途经的 \(6\) 号火车站(亦为 \(2\) 级)而不满足要求。

img/Pasted image 20260618110206.png

现有 \(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

【核心思想】

  1. 问题分析:给定 \(n\) 个车站和 \(m\) 趟车次的停靠情况,每趟车次满足:如果停靠了车站 \(x\),则区间内的所有级别大于等于 \(x\) 的车站都必须停靠。需要求最少需要几个不同的级别。这是一个拓扑排序求最长路问题,关键在于根据停靠关系建立级别大小的约束图。

  2. 算法选择

    • 约束分析:对于每趟车次,在途经区间 \([start, end]\) 内,停靠的站级别高,没停靠的站级别低
    • 建图优化:使用虚拟节点减少边数
      • 虚拟节点 \(vir = n + i\) 代表第 \(i\) 趟车次
      • 没停靠的站 \(j \rightarrow vir\)(权值 \(0\)):表示 \(j\) 的级别 \(\leq\) 车次代表的级别
      • \(vir \rightarrow\) 停靠的站 \(j\)(权值 \(1\)):表示车次代表的级别 \(< j\) 的级别
    • 拓扑排序:求最长路,最长链的长度即为最少级别数
  3. 关键步骤

    • 读取输入\(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)
    • 拓扑排序求最长路
      • 初始化所有节点层级为 \(1\)
      • 入度为 \(0\) 的节点入队
      • BFS:更新邻接节点层级 lvl[j] = max(lvl[j], lvl[u] + w)
    • 计算答案:在所有原站点中取最大层级
    • 输出结果:最少级别数 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m + \sum s_i)\),其中 \(s_i\) 是第 \(i\) 趟车次的站点数
    • 空间复杂度:\(O(n + m + \sum s_i)\),存储图结构
  5. 拓扑排序求最长路的核心思想

    • 约束建图:将级别大小关系转化为有向边,级别低的指向级别高的
    • 虚拟节点优化:避免没停靠站向所有停靠站连边,通过虚拟节点将边数从 \(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
posted @ 2026-06-18 18:03  团爸讲算法  阅读(7)  评论(0)    收藏  举报