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

tarjan提出了很多算法.本文讨论的是图论中求解强连通分量的那个tarjan算法...的应用。

讲得不会非常基础,甚至只是起到记录知识的作用.

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

本文讨论的核心是

  • 有向图为何要缩点

  • 什么是有向图缩点

  • 有向图缩点的实现细节

  • hihoCoder-1185 的实现代码


本文暂时(也可能是永久)不涉及

  • tarjan算法的正确性证明

  • 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>[]的邻接表存边法,所以效率并不是十分高.

各种细节一如前文所述.

以后有空再加一份用链式前向星(我们通常叫做链表)做邻接表的代码.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值