tarjan提出了很多算法.本文讨论的是图论中求解强连通分量的那个tarjan算法...的应用。
讲得不会非常基础,甚至只是起到记录知识的作用.
建议先阅读他人的文章,在对tarjan算法有了大概了解后再继续读下去.
本文讨论的核心是
有向图为何要缩点
什么是有向图缩点
有向图缩点的实现细节
hihoCoder-1185 的实现代码
本文暂时(也可能是永久)不涉及
tarjan算法的正确性证明
tarjan算法的理解
引例
例题:hihoCoder-1185
考虑一个有向图,起始点为1,每个点用正整数编号.给出连通关系以及各节点权值.权值都是自然数.
问: 从1点出发,终点随意,最大路上节点权和可以是多少?
例如: 1->2 就是个合法路径. 这个路径的权值和是6.
如[1]所示.方括号内的数字是此节点的权值.
先考虑暴力算法,DFS.但直接搜下去会死循环.
为什么呢?这是因为这个有向图中有环.
3->6
和 6->3
这两条边使得3和6处在一个环内.两个点强连通.
如果不加处理地从1点开始DFS,必定会在3和6之间来回搜索,因为这样路上的节点权和就会无限增长.
有向图为何要缩点
那么,把所有的"环"都收缩为一个点,那就成了有向无环图(DAG)了.该DP就DP,该DFS就 DFS,毫无困扰了.
什么是有向图缩点
缩点之后是这样的:
这就把6号点"合并"进了3号点.合并之后,3号点和6号点以后等价.
为了方便,我们用3号点来"代表"6号点和环内其他的点,如果有的话
.
这个思想和并查集中"代表元"的思想很像.并且我们约定:编号为n
的节点的"代表元"是contract[n]
.如果节点n
不在环中,为了一般性,令contract[n] = n
,即自己的代表元为自己.这与并查集的约定相同.
有向图缩点的实现细节
将一堆点"合并"为一个"代表元"要修改的有两个东西.一个是边,一个是权.
修改边和权,具体实现起来有两种风格.
修改现成的图
重新建图
笔者喜欢修改图.重新建图是弱者的行为(笑).
首先,用tarjan算法找出从1点出发能到达的所有环...当然每次只会找出一个环.我们说过,每个环都有且仅有一个"代表元".
接着对于这个环上的每个非代表元节点,
把它的所有出边复制给"代表元"后删除.
把它的权值加到代表元上.
你可能会想到: 出边全部删除了,那"其他节点"进入环中非代表元节点的边怎么办呢?换言之,非代表元节点的入边怎么解决?
很简单,对于图中的每条入边指向的节点编号k
,都令其等于contract[k]
.
正所谓"有则改之无则加勉"(逃
hihoCoder-1185 的实现代码
//AC, 116ms
#include
#define ll long long
using namespace std;
const int maxn = 2e4 + 100;
vector e[maxn];
int ins[maxn], dfn[maxn], low[maxn], contract[maxn];
ll w[maxn];
int ind;
stack s;
void tarjan(int u) {
dfn[u] = low[u] = ++ind;
ins[u] = 1;
s.push(u);
for(int i = 0; i < e[u].size(); i++) {
int v = e[u][i];
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if(ins[v]) low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u]) {
int v;
do {
v = s.top();
s.pop();
ins[v] = 0;
contract[v] = u;
if(u != v) {
w[u] += w[v];
while(!e[v].empty()) {
e[u].push_back(e[v].back());
e[v].pop_back();
}
}
} while(u != v);
}
}
ll dfs(int u, ll cnt) {
cnt += w[u];
ll re = cnt;
for(int i = 0; i < e[u].size(); i++) {
int v = contract[e[u][i]];
if(v != u) re = max(re, dfs(v, cnt));
}
return re;
}
int main() {
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> w[i];
}
for(int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
}
tarjan(1);
cout << dfs(1, 0) << endl;
return 0;
}
因为这份代码用的是vector
的邻接表存边法,所以效率并不是十分高.
各种细节一如前文所述.
以后有空再加一份用链式前向星(我们通常叫做链表)做邻接表的代码.