东方博宜OJ 3034:夺宝奇兵 ← 缩点 + Kosaraju 算法

【题目来源】
https://oj.czos.cn/p/3034

【题目描述】
在一个由 N 个城堡和 M 条有向道路构成的迷宫中,每个城堡都存有一定数量的宝石。
小 A 乘坐直升机,可以空降到任何一个地点,他可以从这个地点出发,沿着有向道路行走(可以重复的经过某个城堡,也可以重复的走某条道路),请问他最多能收集多少颗宝石?

【输入格式】
第 1 行有两个正整数 N,M。 第 2 行有 N 个空格隔开的整数,其中第 i 个整数 Ai,表示第 i 个城堡中宝石的数量。 接下来 M 行,每行有两个整数 Xi,Yi 表示 Xi 到 Yi 之间有一条有向道路。

【输出格式】
输出小 A 最多能收集到的宝石数量。

【输入样例】
5 5
2 4 6 10 8
1 2
2 3
3 4
5 4
4 2

【输出样例】
28

【数据范围】
对于 100% 的数据,1≤N, M≤10^5,0≤Ai≤10^3,1≤Xi, Yi≤N。

【算法分析】
● 核心概念
(1)强连通分量(SCC):分量内任意两点互相可达。
(2)缩点:把每个强连通分量(SCC)缩成一个点,原图变成有向无环图(DAG)。
(3)缩点后建新有向无环图(DAG):不同强连通分量(SCC)之间的边,去重,得到有向无环图(DAG)。

● 核心模型
(1)图中存在环,环内所有点可以全部拿完,把环缩成一个强连通分量(SCC),块权值 = 块内所有宝石之和。
(2)缩点后得到有向无环图(DAG)。问题转化为"有向无环图(DAG)上求从任意起点出发的最长路径(点权和最大值)"。
(3)有向无环图(DAG)最长路径:拓扑排序 + DP。
dpu:到达缩点 u 时能拿到的最大宝石。
dpu=valu+max(dpv∣v→u),每个块至少取自身权值。

● 关键点说明
(1)为什么不用递归 Tarjan:N=10^5,递归深度可能爆系统栈,直接 RE。
Kosaraju 两套 DFS,在 OJ 环境下一般栈够用;如果遇到极端栈小 OJ,Kosaraju 也要改成非递归 DFS。
(2)缩点建 DAG 必须去重边:原图多条不同边映射到同一对缩点,会造成入度重复累加,DP 出错。代码用 unique 去重。
(3)DP 初始化:dpi=vali,空降直接落到该块,至少拿本块全部宝石。
(4)拓扑松弛:dpv=max(dpv, dpu+valv);
(5)答案取全部 dp 的最大值,可以空降任意块。

● 本题算法流程总结
(1)Kosaraju 算法求 SCC,每个 SCC 计算块宝石总和;
Kosaraju 算法分两轮 DFS:++首先,DFS(dfs1,跑原图 G),完成后得到 post 序列。然后,
逆序遍历 post 序列,在逆图 G2 上做 dfs2,找出所有强连通分量 SCC++ 。其中:
post\[\]:数组,保存节点编号,存后序序列。
post_cnt:整型计数器,是 post 数组的写入下标,记录当前存了多少个点。

以下是纯 Kosaraju 算法模板代码。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
vector<int> G[N],G2[N];
int st[N];
int post[N],post_cnt;
int scc[N],scc_cnt;

void dfs1(int u) {
    st[u]=1;
    for(int v:G[u]) {
        if(!st[v]) dfs1(v);
    }
    post[++post_cnt]=u;
}

void dfs2(int u) {
    st[u]=1;
    scc[u]=scc_cnt;
    for(int v:G2[u]) {
        if(!st[v]) dfs2(v);
    }
}

void kosaraju(int n) {
    memset(st,0,sizeof st);
    post_cnt=0;
    for(int i=1; i<=n; i++) {
        if(!st[i]) dfs1(i);
    }

    memset(st,0,sizeof st);
    scc_cnt=0;
    for(int i=post_cnt; i>=1; i--) {
        int u=post[i];
        if(!st[u]) {
            ++scc_cnt;
            dfs2(u);
        }
    }
}

int main() {
    int n,m;
    cin>>n>>m;
    for(int i=1; i<=m; i++) {
        int x,y;
        cin>>x>>y;
        G[x].push_back(y);
        G2[y].push_back(x);
    }

    kosaraju(n);
    cout<<scc_cnt<<endl;

    return 0;
}

(2)缩点建 DAG,对重复边去重,统计入度;
(3)拓扑排序,DAG 上 DP 求点权最长路径;
(4)dp 数组最大值即为答案。

【算法代码】

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=1e5+5;

vector<int> G[N],G2[N]; //原图、逆图
vector<int> dag[N]; //缩点后的DAG
int a[N]; //原点点权
int st[N];
int post[N],post_cnt;
int scc[N],scc_cnt;
LL val[N]; //每个scc块的总权值
int in[N]; //inDegree
LL dp[N];

void dfs1(int u) {
    st[u]=1;
    for(int v:G[u]) {
        if(!st[v]) dfs1(v);
    }
    post[++post_cnt]=u;
}

void dfs2(int u) {
    st[u]=1;
    scc[u]=scc_cnt;
    val[scc_cnt]+=a[u];
    for(int v:G2[u]) {
        if(!st[v]) dfs2(v);
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n,m;
    cin>>n>>m;
    for(int i=1; i<=n; i++) {
        cin>>a[i];
    }

    for(int i=1; i<=m; i++) {
        int x,y;
        cin>>x>>y;
        G[x].push_back(y);
        G2[y].push_back(x);
    }

    //Kosaraju第一遍
    memset(st,0,sizeof st);
    post_cnt=0;
    for(int i=1; i<=n; i++) {
        if(!st[i]) dfs1(i);
    }

    //第二遍逆序遍历逆图
    memset(st,0,sizeof st);
    scc_cnt=0;
    for(int i=post_cnt; i>=1; i--) {
        int u=post[i];
        if(!st[u]) {
            ++scc_cnt;
            dfs2(u);
        }
    }

    //构建缩点DAG,去重边
    for(int u=1; u<=n; u++) {
        for(int v:G[u]) {
            if(scc[u]!=scc[v]) {
                dag[scc[u]].push_back(scc[v]);
            }
        }
    }

    //去重边,否则入度统计错误
    for(int i=1; i<=scc_cnt; i++) {
        sort(dag[i].begin(),dag[i].end());
        auto last=unique(dag[i].begin(),dag[i].end());
        dag[i].erase(last,dag[i].end());
        for(int v:dag[i]) in[v]++;
    }

    //拓扑排序
    stack<int> q;
    for(int i=1; i<=scc_cnt; i++) {
        dp[i]=val[i];
        if(in[i]==0) q.push(i);
    }

    LL ans=0;
    while(!q.empty()) {
        int u=q.top();
        q.pop();
        ans=max(ans,dp[u]);
        for(int v:dag[u]) {
            if(dp[v]<dp[u]+val[v]) {
                dp[v]=dp[u]+val[v];
            }
            in[v]--;
            if(in[v]==0) q.push(v);
        }
    }
    cout<<ans<<endl;

    return 0;
}

/*
in:
5 5
2 4 6 10 8
1 2
2 3
3 4
5 4
4 2

out:
28
*/

【参考文献】
https://0xcjy.github.io/oi/graph/suodian.html

相关推荐
h_a_o777oah16 天前
【图论】Tarjan 缩点:解决有向图中环的问题
c++·算法·图论·acm·强连通分量·缩点·tarjan
scx201310048 个月前
20251222强连通分量总结
强连通分量·缩点·边双连通分量
XuYueming1 年前
圆方树学习笔记 —— 一种关于点双连通分量的思考方式
题解·lca·缩点·tarjan·圆方树·点双连通分量·树链剖分·记录 & 心得·仙人掌·理论 / 算法·bzoj·hydro·图的连通性·动态 dp / ddp
闻缺陷则喜何志丹1 年前
【强连通分量 缩点 拓扑排序】P3387 【模板】缩点|普及+
c++·算法·拓扑排序·洛谷·强连通分量·缩点
Qres8213 年前
缩点+图论路径网络流:1114T4
图论·网络流·缩点