洛谷 - P3387 【模板】缩点

模板题

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

// 定义常量:N为最大节点数,M为最大边数
const int N = 1e4 + 10, M = 1e5 + 10;

int n, m;                  // n表示节点数,m表示边数
int e[M], ne[M], h[N], idx; // 邻接表存储图:e[]存储边的终点,ne[]存储下一条边的索引,h[]存储头节点,idx为边的计数器
int low[N], dfn[N], timestamp; // low[]存储节点的最低可达时间戳,dfn[]存储节点的发现时间戳,timestamp为时间戳计数器
int cnt;                   // 强连通分量(SCC)的数量
int stk[N], top;           // 栈stk[]用于存储当前路径上的节点,top为栈顶指针
bool instk[N];             // 标记节点是否在栈中
int scc[N], size[N];       // scc[]存储节点所属的SCC编号,size[]存储每个SCC的节点数量
int w[N], nw[N], dp[N];    // w[]为原图节点权重,nw[]为缩点后每个SCC的总权重,dp[]用于动态规划求最长路径
int x[M], y[M];            // 存储原图的所有边,用于后续缩点后重建新图

// 向邻接表中添加一条从a到b的边
void add(int a, int b) {
    e[idx] = b;          // 存储边的终点
    ne[idx] = h[a];      // 下一条边指向当前a的头节点
    h[a] = idx++;        // 更新a的头节点为当前边的索引,边计数器加1
}

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

    // 遍历当前节点的所有邻接边
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];              // 获取邻接节点v

        if (!dfn[v]) {             // 如果v未被访问过
            tarjan(v);             // 递归访问v
            // 更新当前节点的最低可达时间为其自身和v的最低可达时间的最小值
            low[u] = min(low[u], low[v]);
        } else if (instk[v]) {     // 如果v已被访问且在栈中(说明在当前路径上)
            // 更新当前节点的最低可达时间为其自身和v的发现时间的最小值
            low[u] = min(low[u], dfn[v]);
        }
    }

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

int main() {
    memset(h, -1, sizeof h);       // 初始化邻接表头节点为-1(表示无邻接边)
    scanf("%d%d", &n, &m);         // 读取节点数和边数

    for (int i = 1; i <= n; i++)   // 读取每个节点的权重
        scanf("%d", &w[i]);

    for (int i = 1; i <= m; i++) { // 读取每条边的起点和终点,并添加到邻接表
        scanf("%d%d", &x[i], &y[i]);
        add(x[i], y[i]);
    }

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

    // 重新初始化邻接表,用于构建缩点后的新图
    memset(h, -1, sizeof h);
    idx = 0;

    // 计算每个强连通分量的总权重(缩点后的节点权重)
    for (int i = 1; i <= n; i++)
        nw[scc[i]] += w[i];

    // 重建新图:将原图中的边转换为缩点后的边(不同SCC之间的边)
    for (int i = 1; i <= m; i++) {
        // 如果边的起点和终点属于不同的强连通分量,则在新图中添加一条边
        if (scc[x[i]] != scc[y[i]])
            add(scc[x[i]], scc[y[i]]);
    }

    // 初始化动态规划数组:每个SCC的初始最长路径为其自身的总权重
    for (int i = 1; i <= cnt; i++)
        dp[i] = nw[i];

    // 按强连通分量编号从大到小遍历(拓扑序),更新最长路径
    for (int u = cnt; u >= 1; u--) {
        // 遍历当前SCC的所有邻接边
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];          // 邻接的SCC编号
            // 更新v的最长路径为当前值与u的最长路径加上v的权重的最大值
            dp[v] = max(dp[v], dp[u] + nw[v]);
        }
    }

    // 寻找所有SCC的最长路径中的最大值,即为答案
    int ans = -0x3f3f3f3f;
    for (int i = 1; i <= cnt; i++)
        ans = max(ans, dp[i]);

    printf("%d\n", ans);           // 输出答案
    return 0;
}
posted @ 2025-08-19 10:36  九三青梧  阅读(5)  评论(0)    收藏  举报