洛谷 - P2812 校园网络【[USACO]Network of Schools加强版】

这个题其实是洛谷 - P2746的加强版,只是改了数据范围。

详细分析思路见这里

#include <cstdio>      // 用于输入输出函数,如scanf、printf
#include <cstring>     // 用于内存操作函数,如memset
#include <algorithm>   // 用于算法函数,如min
using namespace std;  // 引入标准命名空间,避免每次使用std::前缀

// 常量定义
const int N = 1e4 + 10;  // 最大学校数量(节点数),1e2=100,+10是为了防止数组越界
const int M = N * N;     // 最大边数,每个节点最多连接到其他所有节点,故为N*N

int n;  // 实际学校数量(节点数)

// 邻接表存储图
int e[M];      // 存储每条边的终点
int ne[M];     // 存储下一条边的索引,用于链表结构
int h[N];      // 存储每个节点的第一条边的索引,初始化为-1表示无第一条边
int idx;       // 边的计数器,用于给新边分配索引

// Tarjan算法相关变量
int dfn[N];    // 存储每个节点的深度优先搜索次序(时间戳)
int low[N];    // 存储每个节点能回溯到的最早时间戳
int timestamp; // 时间戳计数器,用于记录访问顺序

// 用于强连通分量(SCC)的栈
int stk[N];    // 存储当前路径上的节点
int top;       // 栈顶指针

bool instk[N]; // 标记节点是否在栈中

// 强连通分量相关变量
int scc[N];    // 存储每个节点所属的强连通分量编号
int size[N];   // 存储每个强连通分量的大小(包含的节点数)
int cnt;       // 强连通分量的计数器

// 缩点后的有向图的入度和出度
int indegree[N];  // 每个强连通分量的入度
int outdegree[N]; // 每个强连通分量的出度

// 向邻接表中添加一条从a到b的有向边
void add(int a, int b) {
    e[idx] = b;      // 第idx条边的终点是b
    ne[idx] = h[a];  // 第idx条边的下一条边是a当前的第一条边
    h[a] = idx++;    // 更新a的第一条边为第idx条边,然后idx自增
}

// Tarjan算法,用于寻找强连通分量
void tarjan(int u) {
    dfn[u] = low[u] = ++timestamp;  // 初始化当前节点的dfn和low为当前时间戳,并递增时间戳
    stk[++top] = u;                 // 将当前节点入栈
    instk[u] = true;                // 标记当前节点在栈中

    // 遍历当前节点的所有邻接边
    for (int i = h[u]; ~i; i = ne[i]) {  // ~i 等价于 i != -1,遍历链表直到结束
        int v = e[i];  // 获取边的终点v

        if (!dfn[v]) {  // 如果v还未被访问过
            tarjan(v);  // 递归访问v
            // 更新当前节点的low值为自身low和v的low中的较小值
            low[u] = min(low[u], low[v]);
        } 
        // 如果v已被访问且在栈中(说明在当前强连通分量中)
        else if (instk[v]) {
            // 更新当前节点的low值为自身low和v的dfn中的较小值
            low[u] = min(low[u], dfn[v]);
        }
    }

    // 如果当前节点是其所在强连通分量的根节点(dfn等于low)
    if (dfn[u] == low[u]) {
        cnt++;  // 强连通分量数量加1
        int v;  // 用于临时存储出栈的节点
        // 将栈中从当前节点开始的所有节点弹出,这些节点构成一个强连通分量
        do {
            v = stk[top--];      // 栈顶节点出栈
            instk[v] = false;    // 标记节点不在栈中
            scc[v] = cnt;        // 记录节点v所属的强连通分量编号
            size[cnt]++;         // 该强连通分量的大小加1
        } while (v != u);  // 直到弹出的节点是当前节点u为止
    }
}

int main() {
    memset(h, -1, sizeof h);  // 初始化邻接表头,所有节点初始无第一条边
    scanf("%d", &n);          // 读取学校数量n

    // 读取每个学校的连接关系
    for (int a = 1; a <= n; a++) {  // 遍历每个学校a(1-based索引)
        while (true) {
            scanf("%d", &b);  // 读取a指向的学校b
            if (b == 0) break;  // 若b为0,结束当前学校的输入
            add(a, b);  // 添加一条从a到b的有向边
        }
    }

    // 对所有未访问的节点执行Tarjan算法,寻找强连通分量
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {  // 如果节点i未被访问过
            tarjan(i);  // 对i执行Tarjan算法
        }
    }

    // 计算缩点后每个强连通分量的入度和出度
    for (int u = 1; u <= n; u++) {  // 遍历每个节点u
        // 遍历u的所有邻接边
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];  // 边的终点v
            // 如果u和v属于不同的强连通分量,则说明两个分量之间有边
            if (scc[u] != scc[v]) {
                indegree[scc[v]]++;   // v所在分量的入度加1
                outdegree[scc[u]]++;  // u所在分量的出度加1
            }
        }
    }

    // 统计入度为0和出度为0的强连通分量数量
    int in = 0, out = 0;
    for (int i = 1; i <= cnt; i++) {
        if (indegree[i] == 0) in++;   // 入度为0的分量数
        if (outdegree[i] == 0) out++; // 出度为0的分量数
    }

    // 输出第一个答案:至少需要添加的边数,使所有节点能被到达(入度为0的分量数)
    printf("%d\n", in);

    // 输出第二个答案:至少需要添加的边数,使整个图成为强连通图
    if (cnt == 1) puts("0");  // 如果只有一个强连通分量,不需要添加边
    else printf("%d\n", max(in, out));  // 否则为入度0和出度0的分量数的最大值

    return 0;
}
posted @ 2025-08-18 17:40  九三青梧  阅读(7)  评论(0)    收藏  举报