题解:P3387 【模板】缩点
这篇题解适合对 Tarjan 算法已有大致了解但还不够熟悉,且面对此题毫无头绪的读者阅读,不太适合完全的初学者
深度思考部分
题意概括
有一张有向图,在上面寻找一条最长路,一个点的权值只计算一次。
算法分析
仔细读题就能发现,这个图有环,直接跑最长路会卡住,所以,我们要使用缩点。
缩点是什么?顾名思义,把有向图的多个点组成的环收缩成一个点的过程就是缩点。缩点结束后这个图就没环了,直接最长路就可以解决。
缩点怎么实现?Tarjan。
Tarjan
已经熟知 Tarjan 的可以跳过该部分。
Tarjan 算法可以找出图中所有强连通分量,即环,进而进行缩点,下面我来说说 Tarjan 到底是什么。
有一张有向图,从任意一顶点对它跑 DFS,不经过重复点,把遍历时经过的边构建成一棵树,我们称这棵树为 DFS 生成树。
DFS 生成树中的树边可以分为以下三种:
-
前向边 指向前遍历新节点产生的边;
-
返祖边 指遍历到头返回祖先的边;
-
横叉边 指在遍历过程中遍历到已经遍历过的点,且这个点不是它的祖先的边。
我们使用对它使用 Tarjan 遍历。
在遍历时,我们需要维护一个栈
我们设点
每遍历到一个节点,就计算他的
接下来,依次遍历他的所有边,这会出现三种情况:
- 前向边 未遍历过的新节点(判断一下
dfn_i 是否有值),那么直接遍历该节点,该节点可以回溯目标节点,所以在遍历完成后还要更新当前节点的low_i - 返祖边 已经在栈中的节点(判断一下目标节点是否入栈
inst_i=true ),那么该节点可以回溯目标节点,更新该节点的low_i - 横叉边 无意义,不做任何事情
在判断完所有边后,如果
这样进行 Tarjan 直到遍历结束,这样就可以找到图中所有的强连通分量,即环,并染色完毕。
缩点
在 Tarjan 染色完毕后,构建一张新图,把所有一个颜色的节点缩成一个节点,缩点结束后,这个图已经变成了一个有向无环图,接下来,最长路登场
最长路 拓扑排序
实现最长路可以使用 SPFA 或拓扑排序,因为 SPFA 在特殊情况下卡常,所以这里写的是拓扑排序。 拓扑排序也就不在过多赘述,只是注意一下拓扑的转移方程不要写错即可
代码部分
#include <iostream>
#include <vector>
#include <stack>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e4 + 5;
// 变量定义
// val 点权 nval 新图点权
// dfn DFS序
// low 最早能通过返祖边回溯到的已经在栈中的节点时间
// color 颜色 scc 强连通分量个数
// 栈 st 存储正在遍历的节点 inst 节点是否入栈
// g 和 ng 原图和新图
// indu 入度
int n, m, val[N], s1, s2;
int dfn[N], low[N], inst[N], ti, dp[N];
int scc, color[N], nval[N], indu[N], maxn = 0;
vector<int> g[N], ng[N];
stack<int> st;
void tarjan(int x)
{
// 计算 dfn 和 low
dfn[x] = low[x] = ++ti;
// 入栈
st.push(x), inst[x] = 1;
// 遍历所有的边
for (auto y : g[x]) {
if (!dfn[y]) { // 前向边
tarjan(y); // 继续遍历
low[x] = min(low[x], low[y]); // 更新
} else if (inst[y]) { // 返祖边
low[x] = min(low[x], dfn[y]); // 更新
}
}
// 是否为强连通分量起点
if (dfn[x] == low[x]) {
int y;
scc++; // 强联通分量个数++
do {
// 出栈
y = st.top(), st.pop();
inst[y] = 0;
// 染色
color[y] = scc;
nval[scc] += val[y];
} while (y != x);
// 重复出栈直到栈中该节点上方的节点全部出完栈
}
}
void topo()
{
queue<int> q;
for (int i = 1; i <= scc; i++) { // 初始化
if (!indu[i]) q.push(i), dp[i] = nval[i];
}
while (!q.empty()) {
int x = q.front();
maxn = max(maxn, dp[x]);
q.pop();
for (auto y : ng[x]) {
dp[y] = max(dp[y], dp[x] + nval[y]);
if (--indu[y] == 0) q.push(y);
}
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &val[i]);
for (int i = 1; i <= m; i++) {
scanf("%d %d", &s1, &s2);
g[s1].push_back(s2); // 建图
}
for (int i = 1; i <= n; i++) { // Tarjan
if (!dfn[i]) {
tarjan(i);
}
}
// 建新图
for (int x = 1; x <= n; x++) {
for (auto y : g[x]) {
if (color[x] != color[y]) { // 不属于同一个强连通分量
ng[color[x]].push_back(color[y]);
indu[color[y]]++;
}
}
}
// 拓扑算最短路
topo();
printf("%d", maxn);
return 0;
}