洛谷题单指南-连通性问题-P3387 【模板】缩点
原题链接:https://www.luogu.com.cn/problem/P3387
题意解读:求一条路径,使路径经过的点权值之和最大,路径上点可以重复但每个点权值只算一次,输出最大的权值和。
解题思路:
1、问题分析
如果是DAG,可以直接通过拓扑排序来进行递推,计算到每个点的路径权值之和的最大值。
但是,如果图中存在环,则需要进行缩点处理,缩点的原理请参考:https://www.cnblogs.com/hackerchef/p/18851009
2、算法流程
计算所有强联通分量
完成缩点,缩点的权值是该强联通分量所有点权值之和
建立新图
在新图上跑一遍拓扑排序,然后递推出到每个点的路径权值之和的最大值
小技巧:对于Tarjan算法所得到的强联通分量,按照序号逆序天然具有拓扑序。
为什么?
这是因为拓扑排序除了用BFS,同样可以用DFS实现,具体实现逻辑是:
后序遍历:对图中每一个未访问过的节点进行DFS遍历,在递归返回时将节点加入结果列表
逆序输出:最终将后序遍历结果反转即得到拓扑排序
为什么这样可行?
DFS确保所有子孙节点先被处理
后序添加保证依赖关系被满足
反转后得到正确的依赖顺序
因此,直接逆序枚举所有强连通分量编号对应的缩点,再枚举缩点的所有邻边,进行递推更新即可。
100分代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 10005, M = 200005;
int h1[N], h2[N], e[M], ne[M], idx; //h1:原图 h2:缩点后的图
int dfn[N], low[N], timestamp;
stack<int> stk;
bool vis[N];
int scc[N], cnt;
int a[N]; //原图点权值
int sum[N]; //sum[i]表示强连通分量i中所有点权值之和
int f[N]; //f[i]表示以i为终点的路径上权值之和的最大值
int n, m;
void add(int h[], int a, int b)
{
e[++idx] = b;
ne[idx] = h[a];
h[a] = idx;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;
stk.push(u);
vis[u] = true;
for(int i = h1[u]; i; i = ne[i])
{
int v = e[i];
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(vis[v])
{
low[u] = min(low[u], dfn[v]);
}
}
if(low[u] == dfn[u])
{
cnt++;
while(true)
{
int t = stk.top(); stk.pop();
vis[t] = false;
scc[t] = cnt;
sum[cnt] += a[t]; //强联通分量内点的权值累加
if(t == u) break;
}
}
}
void build()
{
for(int i = 1; i <= n; i++)
{
for(int j = h1[i]; j; j = ne[j])
{
int u = scc[i], v = scc[e[j]];
if(u != v)
add(h2, u, v);
}
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
add(h1, u, v);
}
//计算强联通分量
for(int i = 1; i <= n; i++)
if(!dfn[i])
tarjan(i);
//缩点建图
build();
//递推求f[]
for(int i = cnt; i >= 1; i--)
{
if(!f[i]) f[i] = sum[i]; //初始化
for(int j = h2[i]; j; j = ne[j])
{
int v = e[j];
if(f[v] < f[i] + sum[v])
{
f[v] = f[i] + sum[v];
}
}
}
//找最大值
int ans = 0;
for(int i = 1; i <= cnt; i++) ans = max(ans, f[i]);
cout << ans;
return 0;
}