【题目来源】
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
*/