图论学习笔记 - Tarjan 算法与有向图连通性

前言

给定有向图 G = (V, E),若存在 r ∈ V,满足从 r 出发能够到达 V 中所有的点,则称 G 是一个“流图”(Flow Graph),记为(G, r),其中 r 称为流图的源点。

与无向图的深度优先遍历类似,我们也可以定义“流图”的搜索树和时间戳的概念:

在一个流图(G, r) 上从 r 出发进行深度优先遍历,每个点只访问一次。所有发生递归的边 (x, y)(换言之,从 x 到 y 是对 y 的第一次访问)构成一棵以 r 为根的树,我们把它称为流图 (G, r) 的搜索树

同时,在深度优先遍历的过程中,按照每个节点第一次被访问的时间顺序,依次给予流图中 N 个节点 1~N 的整数标记,该标记被称为时间戳,记为 dfn[x]。

流图中的每条有向边 (x, y) 必然是以下四种之一:

1. 树枝边,指搜索树中的边,即 x 是 y 的父节点。

2. 前向边,指搜索树中 x 是 y 的祖先节点。

3. 后向边,指搜索树中 y 是 x 的祖先节点。

4. 横叉边,指除了以上三种情况之外的边,一定满足 dfn[y] < dfn[x]。

有向图的强连通分量

给定一张有向图。若对于图中任意两个节点 x,y,既存在从 x 到 y 的路径,也存在从 y 到 x 的路径,则称该有向图是“强连通图”。

有向图的极大强连通子图被称为“强连通分量”,简记为 SCC。此处“极大”的含义与双连通分量“极大”的含义类似。

Tarjan 算法基于有向图的深度优先遍历,能够在线性时间求出一张有向图的各个强连通分量。

一个“环”一定是强连通图。如果既存在从 x 到 y 的路径,也存在从 y 到 x 的路径,那么 x,y 显然在一个环中。因此,Tarjan 算法的基本思路就是对于每个点,尽量找到与它一起能构成环的所有节点。

容易发现,“前向边” (x, y) 没有什么用处,因为搜索树上本来就存在从 x 到 y 的路径。“后向边” (x, y) 非常有用,因为它可以和搜索树上从 y 到 x 的路径一起构成环。“横叉边” (x, y) 视情况而定,如果从 y 出发能找到一条路径回到 x 的祖先节点,那么 (x, y)  就是有用的。

为了找到通过“后向边”和“横叉边”构成的环,Tarjan 算法在深度优先遍历的同时维护了一个栈。当访问到节点 x 时,栈中需要保存以下两类节点:

1. 搜索树上 x 的祖先节点,记为集合 anc(x)。

设 y∈anc(x)。若存在后向边 (x, y),则 (x, y) 与 y 到 x 的路径一起形成环。

2. 已经访问过,并且存在一条路径到达 anc(x) 的节点。

设 z 是一个这样的点,从 z 出发存在一条路径到达 y∈anc(x)。若存在横叉边 (x, z),则 (x, z)、z 到 y 的路径、y 到 x 的路径形成一个环。

综上所述,栈中的节点就是能与从 x 出发的“后向边”和“横叉边”形成环的节点。进而可以引入“追溯值”的概念。

追溯值

设 subtree(x) 表示流图的搜索树中以 x 为根的子树。x 的追溯值 low[x] 定义为满足以下条件的节点的最小时间戳:

1. 该点在栈中。

2. 存在一条从 subtree(x) 出发的有向边,以该点为终点。

根据定义,Tarjan 算法按照以下步骤计算“追溯值”:

1. 当节点 x 第一次被访问时,把 x 入栈,初始化 low[x] = dfn[x]。

2. 扫描从 x 出发的每条边 (x, y)。

        (1) 若 y 没被访问过,则说明 (x, y) 是树枝边,递归访问 y,从 y 回溯之后,令 low[x] = min(low[x], low[y])。

        (2) 若 y 被访问过并且 y 在栈中,则令 low[x] = min(low[x], dfn[y])。

3. 从 x 回溯之前,判断是否有 low[x] = dfn[x]。若成立,则不断从栈中弹出节点,直至 x 出栈。

强连通分量判定法则

在追溯值的计算过程中,若从 x 回溯前,有 low[x] = dfn[x] 成立,则栈中从 x 到栈顶的所有节点构成一个强连通分量。

我们不再详细证明。大致来说,在计算追溯值的第 3 步,如果 low[x] = dfn[x],那么说明 subtree(x) 中的节点不能与栈中其他节点一起构成环。另外,因为横叉边的终点时间戳必定小于起点时间戳,所以 subtree(x) 中的节点也不可能直接到达尚未访问的节点(时间戳更大)。综上所述,栈中从 x 到栈顶的所有节点不能与其他节点一起构成环。

又因为我们及时进行了判定和出栈操作,所以从 x 到栈顶的所有节点独立构成一个强连通分量。

与无向图 e-DCC 的“缩点”类似,我们也可以把每个 SCC 缩成一个点。对原图中的每条有向边 (x,y),若 c[x] ≠ c[y],则在编号为 c[x] 与编号为 c[y] 的 SCC 之间连边。最后,我们会得到一张有向无环图。

有向图的必经点与必经边

给定一张有向图,起点为 S,终点为 T。若从 S 到 T 的每条路径都经过一个点 x,则称点 x 是有向图中从 S 到 T 的必经点。

若从 S 到 T 的每条路都经过一条边 (x, y),则称这条边是有向图中从 S 到 T 的必经边或“桥”。

有向图的必经点与必经边是一个较难的问题。因为环上的点也可能是必经点,所以不能简单地把强连通分量缩点后按照有向无环图来处理。这里不进行具体讲解,请大家自行查阅其他资料。

2 - SAT 问题

有 N 个变量,每个变量只有两种可能的取值。再给定 M 个条件,每个条件都是对两个变量的取值限制。求是否存在对 N 个变量的合法赋值,使 M 个条件均得到满足。这个问题被称为 2 - SAT 问题。SAT 是英语 satisfiability 的缩写,所以 2 - SAT 也可翻译为 “2 - 可满足性” 问题。

设一个变量 Ai(1≤i≤N) 的两种可能取值分别是 A_{i,0}A_{i,1}。在 2 - SAT 问题中, M 个条件都可以转化为统一的形式——“若变量 Ai 赋值为 A_{i, p},则变量 Aj 必须赋值为 A_{j,q}”,其中 p,q∈{0, 1}。

2 - SAT 问题的判定方法如下:

1. 建立 2N 个节点的有向图,每个变量 Ai 对应 2 个节点,一般设为 i 和 i + N。

2. 考虑每个条件,形如“若变量 Ai 赋值为 A_{i, p},则变量 Aj 必须赋值为 A_{j,q}”,p,q∈{0, 1}。从 i + p * N 到 j + q * N 连一条有向边。

注意,上述条件蕴含着它的逆否命题“若变量 Aj 必须赋值为 A_{j,1-q},则变量 Ai 必须赋值为A_{i,1-p}”。如果在给出的 M 个限制条件中,原命题和逆否命题不一定成对出现,应该从 j + (1 - q) * N 到 i + (1 - p) * N 也连一条有向边。

总而言之,根据原命题和逆否命题的对称性,2 -SAT 建出的有向图一定能画成“一侧是节点 1~N,另一侧是节点 N + 1~2N”。当把图中的边看作无向边时,这两侧连边的情况时对称的。

3. 用 Tarjan 算法求出有向图中所有的强连通分量。

4. 若存在 i∈[1, N],满足 i 和 i + N 属于同一个强连通分量,则表明:若变量 Ai 赋值为 A_{i, p},则变量 Ai 必须赋值为 A_{i,1-p}。这显然是矛盾的,说明问题无解。若不存在这样的 i,则问题一定有解。

时间复杂度为 O(N + M)

我们可以用并查集处理二元关系的一类模型,前提是关系具有传递性,并且关系是“无向”的。无向关系可以理解为:从原命题“若 P 则 Q”能推出逆命题“若 Q 则 P”、逆否命题“若非 Q 则非 P”、否命题“若非 P 则非 Q”。在扩展了一倍域的并查集中,对于每条关系我们会添加两条无向边,其实就等价于上述 2 - SAT 的四个命题对应的四条有向边。

而在 2 - SAT 模型中,关系是“有向”的。有向关系就是一般的关系,从原命题“若 P 则 Q”一定能推出逆否命题“若非 Q 则非 P”,但不一定能推出逆命题“若 Q 则 P”、否命题“若非 P 则非 Q”。我们也不用强调关系的传递性,因为“若 P 则 Q”和“若 Q 则 R”两个命题自然能推出命题“若 P 则 R”。总而言之,2 - SAT 模型符合二元关系的一般逻辑,能处理更多、更复杂的问题。

P3387【模板】缩点

#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<stack>
#include<queue>
using namespace std;

const int maxn = 1000100;
int n, m;
int tot;
int cnt;
int sum;
int scc_tot;
int a[maxn];
int sd[maxn];
int p[maxn];
int dfn[maxn];
int low[maxn];
int head[maxn];
int scc_head[maxn];
int vis[maxn];
int in[maxn];
int dis[maxn];

struct edge{
    int to;
    int from;
    int nxt;
}e[2 * maxn], scc_e[2 * maxn];

void add(int x, int y){
    tot++;
    e[tot].to = y;
    e[tot].from = x;
    e[tot].nxt = head[x];
    head[x] = tot;
}

void scc_add(int x, int y){
    scc_tot++;
    scc_e[scc_tot].from = x;
    scc_e[scc_tot].to = y;
    scc_e[scc_tot].nxt = scc_head[x];
    scc_head[x] = scc_tot;
}

stack<int> s;

void tarjan(int x){
    dfn[x] = low[x] = ++cnt;
    s.push(x);
    vis[x] = 1;
    for(int i = head[x]; i; i = e[i].nxt){
        int y = e[i].to;
        if(!dfn[y]){
            tarjan(y);
            low[x] = min(low[x], low[y]);
        }
        else if(vis[y]) low[x] = min(low[x], low[y]);
    }
    if(dfn[x] == low[x]){
        int y;
        while(!s.empty()){
            y = s.top();
            s.pop();
            vis[y] = 0;
            sd[y] = x;
            if(x == y) break;
            a[x] += a[y];
        };
    }
}

int solve(){
    queue<int> q;
    for(int i = 1; i <= n; i++){
        if(sd[i] == i && !in[i]){
            q.push(i);
            dis[i] = a[i];
        }
    }
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = scc_head[u]; i; i = scc_e[i].nxt){
            int v = scc_e[i].to;
            dis[v] = max(dis[v], dis[u] + a[v]);
            in[v]--;
            if(!in[v]) q.push(v);
        }
    }
    int ans = 0;
    for(int i = 1; i <= n; i++) ans = max(ans, dis[i]);
    return ans;
}

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a[i];
    int x, y;
    for(int i = 1; i <= m; i++){
        cin >> x >> y;
        add(x, y);
    }
    for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i);
    for(int i = 1; i <= m; i++){
        x = sd[e[i].from];
        y = sd[e[i].to];
        if(x != y){
            scc_add(x, y);
            in[y]++;
        }
    }
    cout << solve() << '\n';
}
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值