tarjan算法求强连通份量的应用:有向图缩环为点

tarjan提出了不少算法.本文讨论的是图论中求解强连通份量的那个tarjan算法...的应用。c++

讲得不会很是基础,甚至只是起到记录知识的做用.算法

建议先阅读他人的文章,在对tarjan算法有了大概了解后再继续读下去.spa

本文讨论的核心是.net

  • 有向图为什么要缩点code

  • 什么是有向图缩点blog

  • 有向图缩点的实现细节图片

  • hihoCoder-1185 的实现代码ci


本文暂时(也多是永久)不涉及get

  • tarjan算法的正确性证实it

  • tarjan算法的理解

引例

例题:hihoCoder-1185

考虑一个有向图,起始点为1,每一个点用正整数编号.给出连通关系以及各节点权值.权值都是天然数.

问: 从1点出发,终点随意,最大路上节点权和能够是多少?

例如: 1->2 就是个合法路径. 这个路径的权值和是6.

如[1]所示.方括号内的数字是此节点的权值.

有向图的环,图片来自hihoCoder 1185

先考虑暴力算法,DFS.但直接搜下去会死循环.

为何呢?这是由于这个有向图中有环.

3->66->3这两条边使得3和6处在一个环内.两个点强连通.

若是不加处理地从1点开始DFS,一定会在3和6之间来回搜索,由于这样路上的节点权和就会无限增加.

有向图为什么要缩点

那么,把全部的"环"都收缩为一个点,那就成了有向无环图(DAG)了.该DP就DP,该DFS就 DFS,毫无困扰了.

什么是有向图缩点

缩点以后是这样的:

缩点,图片来自hihoCoder 1185

这就把6号点"合并"进了3号点.合并以后,3号点和6号点之后等价.

为了方便,咱们用3号点来"表明"6号点和环内其余的点,若是有的话.

这个思想和并查集中"表明元"的思想很像.而且咱们约定:编号为n的节点的"表明元"是contract[n].若是节点n不在环中,为了通常性,令contract[n] = n,即本身的表明元为本身.这与并查集的约定相同.

有向图缩点的实现细节

将一堆点"合并"为一个"表明元"要修改的有两个东西.一个是边,一个是权.

修改边和权,具体实现起来有两种风格.

  1. 修改现成的图

  2. 从新建图

笔者喜欢修改图.从新建图是弱者的行为(笑).

首先,用tarjan算法找出从1点出发能到达的全部环...固然每次只会找出一个环.咱们说过,每一个环都有且仅有一个"表明元".

接着对于这个环上的每一个非表明元节点,

  1. 把它的全部出边复制给"表明元"后删除.

  2. 把它的权值加到表明元上.

你可能会想到: 出边所有删除了,那"其余节点"进入环中非表明元节点的边怎么办呢?换言之,非表明元节点的入边怎么解决?

很简单,对于图中的每条入边指向的节点编号k,都令其等于contract[k].
正所谓"有则改之无则加勉"(逃

hihoCoder-1185 的实现代码

//AC, 116ms
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 2e4 + 100;
vector<int> e[maxn];
int ins[maxn], dfn[maxn], low[maxn], contract[maxn];
ll w[maxn];
int ind;
stack<int> 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<int>[]的邻接表存边法,因此效率并非十分高.

各类细节一如前文所述.

之后有空再加一份用链式前向星(咱们一般叫作链表)作邻接表的代码.

相关文章
相关标签/搜索