浅谈tarjan算法

塔杨老爷子创造的算法让人头皮发麻,却不得不赞叹他的过人之处----前言

学习tarjan之前我们需要知道一些图论的前置知识

前置知识

强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。

强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。

还有DFS树、DFS序等请前往此处学习,这里不再赘述

tarjan之求强连通分量

首先先来说明一下每个数组的作用

vis[]判断该点是否在栈里

used[]判断该点是否遍历到

dfn[]该点被搜索到的次序编号(时间戳),不难发现每个点的时间戳都不一样

low[]在该点的子树中能够回溯到的最早的已经在栈中的结点

num[]记录每个强连通分量里含有多少个点

color[]记录该点在哪个强连通分量中

G[][]当然是我最爱的vector存图方式啦(链式前向星,达咩!)

从定义可知,从根开始的一条路径上的 dfn 严格递增,low 严格非降。

学习算法怎么能不配合点例题呢

[USACO06JAN]The Cow Prom S - 洛谷

这个题题意很容易,一张有向图里求强连通分量大于1的个数,tarjan的基本操作

该题样例

浅谈tarjan算法_第1张图片

 从这张图中我们不难看出,点集{1,2,4}是一个强连通分量,因为点集中的点两两可以互相到达,而{3}和{5}是两个独立的强连通分量,下面我们通过这个样例来说一下tarjan求强连通分量的过程

首先我们是从点1开始进行tarjan算法,然后点1入栈,开始遍历点2,发现点2并不在栈中,那么就开始遍历点2,又发现点4不在栈中,

那么开始遍历点4,发现点1已经入栈,low[4]=min(low[4],dfn[1]),然后low[4]与dfn[4]不相同,

然后再回溯点2,执行下一行代码,low[2]=min(low[2],low[4]),发现low[2]与dfn[2]也不相同,再回溯点1,low[1]=min(low[1],low[2]),然后此时发现low[1]与dfn[1]相同,说明已经到根,然后就开始对点染色和强连通分量的计算,以及其中点的个数的计算,然后结束这一轮的tarjan算法,发现点3并没有遍历过,再从点3开始重复上述操作

AC代码:

#include 
using namespace std;
using LL = long long;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, ans = 0, idx = 0, cnt = 0;
    cin >> n >> m;
    stack st;
    vector vis(n + 1), used(n + 1), dfn(n + 1), low(n + 1), num(n + 1), color(n + 1);
    vector> G(n + 1);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
    }
    function tarjan = [&](int u) {
        dfn[u] = low[u] = ++idx;
        st.push(u);
        vis[u] = used[u] = true;
        for (auto v : G[u]) {
            if (!dfn[v]) {
                tarjan(v);
                low[u] = min(low[u], low[v]);
            }
            else if (vis[v]) {
                low[u] = min(low[u], dfn[v]);
            }
        }
        if (dfn[u] == low[u]) {
            int z;
            cnt++;
            do {
                z = st.top();
                st.pop();
                color[z] = cnt;
                num[cnt]++;
                vis[z] = false;
            } while (z != u);
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!used[i]) {
            tarjan(i);
        }
    }
    for (int i = 1; i <= cnt; i++) {
        if (num[i] > 1) {
            ans++;
        }
    }
    cout << ans << '\n';
    return 0;
}

tarjan之求缩点

求缩点其实就是建立在前面求强连通分量的基础之上,对将每一个环看成一个点,我们在用tarjan求强连通分量的时候也记录了每个点所在的强连通分量(类似染色),以及每个强连通分量里有多少个点,因此,为了缩点,我们需要遍历每一条边,如果u和v处于不同的强连通分量里,那么说明这两个强连通分量之间有一条有向边,那么我们就把两个强连通分量看成两个不同的点,然后按照建图方式建图就好了(听君一席话,如听一席话

[USACO03FALL / HAOI2006] 受欢迎的牛 G - 洛谷


 

题目的意思是牛A能当明星牛,当且仅当所有牛都喜欢他,而且牛对牛的喜欢可以传递,这就相当于一张由有向边构成的有向图,我们要先求出所有强连通分量来,然后进行缩点,如果缩点以后有出度为0的,那么这个点就有可能是一个或一群明星牛,为什么说可能呢,因为如果出度为0的点大于1个的话,也就是说这些出度为0的点之间是不连通的,那么也就不符合所有牛都喜欢的定义,注意,这个点指的是缩完以后的点,并不是最初定义的编号1-n的点

AC代码:

#include 
using namespace std;
using LL = long long;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, ans = 0, idx = 0, cnt = 0;
    cin >> n >> m;
    stack st;
    vector du(n + 1), vis(n + 1), used(n + 1), dfn(n + 1), low(n + 1), num(n + 1), color(n + 1);
    vector> G(n + 1);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
    }
    function tarjan = [&](int u) {
        dfn[u] = low[u] = ++idx;
        st.push(u);
        vis[u] = used[u] = true;
        for (auto v : G[u]) {
            if (!dfn[v]) {
                tarjan(v);
                low[u] = min(low[u], low[v]);
            }
            else if (vis[v]) {
                low[u] = min(low[u], dfn[v]);
            }
        }
        if (dfn[u] == low[u]) {
            int z;
            cnt++;
            do {
                z = st.top();
                st.pop();
                color[z] = cnt;
                num[cnt]++;
                vis[z] = false;
            } while (z != u);
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!used[i]) {
            tarjan(i);
        }
    }
    for (int i = 1; i <= n; i++) {
        for (auto v : G[i]) {
            if (color[i] != color[v]) {
                du[color[i]]++;
            }
        }
    }
    for (int i = 1; i <= cnt; i++) {
        if (du[i] == 0) {
            ans++;
        }
    }
    if (ans > 1 or ans == 0) {
        cout << "0\n";
    }
    else {
        cout << num[ans] << '\n';
    }
    return 0;
}

讲解缩点怎么会没有洛谷的真·模板题呢?

【模板】缩点 - 洛谷

题意是一张有向图,可以走一个点或者一条边任意遍,然后求一条路径上的最大值

做法

我们可以先用tarjan求出强连通分量,注意求强连通分量的时候把该强连通分量里面的点权也顺便求出来,这样便于求缩完点以后的点权,然后再进行缩点,建图,之后就是对于一般有向图的拓扑排序DP求最大值(感觉代码写的并不是很漂亮QAQ,能力有限)

AC代码:

#include 
using namespace std;
using LL = long long;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, idx = 0, cnt = 0;
    cin >> n >> m;
    stack st;
    vector a(n + 1), in(n + 1), val(n + 1), vis(n + 1), used(n + 1), dfn(n + 1), low(n + 1), num(n + 1), color(n + 1);
    vector> G(n + 1), G1(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
    }
    function tarjan = [&](int u) {
        dfn[u] = low[u] = ++idx;
        st.push(u);
        vis[u] = used[u] = true;
        for (auto v : G[u]) {
            if (!dfn[v]) {
                tarjan(v);
                low[u] = min(low[u], low[v]);
            }
            else if (vis[v]) {
                low[u] = min(low[u], dfn[v]);
            }
        }
        if (dfn[u] == low[u]) {
            int z;
            cnt++;
            do {
                z = st.top();
                st.pop();
                color[z] = cnt;
                num[cnt]++;
                val[cnt] += a[z];
                vis[z] = false;
            } while (z != u);
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!used[i]) {
            tarjan(i);
        }
    }
    for (int i = 1; i <= n; i++) {
        for (auto v : G[i]) {
            if (color[i] != color[v]) {
                G1[color[i]].push_back(color[v]);
                in[color[v]]++;
            }
        }
    }
    queue qu;
    vector ans(cnt + 1);
    for (int i = 1; i <= cnt; i++) {
        if (in[i] == 0) {
            qu.push(i);
        }
    }
    while (!qu.empty()) {
        int u = qu.front();
        qu.pop();
        ans[u] += val[u];
        for (auto v : G1[u]) {
            in[v]--;
            if (in[v] == 0) {
                qu.push(v);
                ans[v] += ans[u];
            }
        }
    }
    cout << *max_element(ans.begin(), ans.end()) << '\n';
    return 0;
}

tarjan之求割点(割顶)

先说一下割点的概念

在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点。

浅谈tarjan算法_第2张图片

从这张图上来分析一下哪些点是割点,根据定义,我们可以知道,其中0和3是割点,当去掉其中任意一个或者全部的点所连的边的话,剩下的图就不再连通, 从直观角度来看,如果一个点拥有两颗及以上子树的话,这个点就一定是一个割点,因为两颗子树能互相到达当且仅当经过这个点,当然这是对根节点而言,那么对于非根节点该如何判断其是否是割点?

这个时候就要%一下塔杨老爷子,tarjan算法巧妙地解决了这个问题,就是使用其中的low[]和dfn[],dfn[u]表示顶点u第几个被首次访问,low[u]表示顶点u及其子树中的点,通过非父子边,能够回溯到的最早的点(dfn最小)的dfn值,对于边(u, v),如果low[v]>=dfn[u],此时u就是割点,

或者说对一个连通图进行DFS,在DFS的过程中如果存在一个非根点不存在返祖边(不能回到父节点之前的点),则该节点的父节点便为割点

附上模板题

【模板】割点(割顶) - 洛谷

AC代码:

#include 
using namespace std;
using LL = long long;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, idx = 0, cnt = 0;
    cin >> n >> m;
    vector> G(n + 1);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    vector cut(n + 1);
    vector dfn(n + 1), low(n + 1);
    function tarjan = [&](int u, int fa) {
        dfn[u] = low[u] = ++idx;
        int child = 0;
        for (auto v : G[u]) {
            if (!dfn[v]) {
                tarjan(v, u);
                low[u] = min(low[u], low[v]);
                if (low[v] >= dfn[u] and u != fa) {
                    cut[u] = true;
                }
                if (u == fa) {
                    child++;
                }
            }
            else if (u != fa) {
                low[u] = min(low[u], dfn[v]);
            }
        }
        if (child >= 2 and u == fa) {
            cut[u] = true;
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, i);
        }
    }
    int tot = 0;
    for (int i = 1; i <= n; i++) {
        if (cut[i]) {
            tot++;
        }
    }
    cout << tot << '\n';
    for (int i = 1; i <= n; i++) {
        if (cut[i]) {
            cout << i << " ";
        }
    }
    cout << '\n';
    return 0;
}

tarjan之点双连通分量

老规矩,先来介绍一下点双连通分量的一些概念

点双连通分量---v-DCC

双连通分量---DCC

边双连通分量---e-DCC

点双连通:若对于一个无向图,其任意一个节点对于这个图本身而言都不是割点,则称其点双连通。也就是说,删除任意点及其相关边后,整个图仍然属于一个连通分量。

点双连通分量:无向图中,极大的点双连通子图。与连通分量类似,抽离出一些点及它们之间的边,使得抽离出的图是一个点双连通图,在这个前提下,使得抽离出的图越大越好。

一张无向连通图是点双连通图当且仅当满足下列两个条件之一:

1.图的顶点不超过2

2.(当图的顶点大于2)图中任意两点都同时包含在至少一个简单环中,其中简单环指的是不自交的环(即自环)

点双连通分量不具有传递性,例如下图

浅谈tarjan算法_第3张图片

 可知1和4点双连通,4和7点双连通,但1和7点双不连通,因为删掉4及其连接的边,1和7将不再连通

为了求出点双连通分量,需要在tarjan过程中维护一个栈,用栈去维护一个点双连通分量中的所有点,维护方法:

1.当一个节点第一次被访问时,把该节点入栈

2.当割点判定法则中的条件dfn[u]<=low[v]成立时,无论u是否为根,都要:

1)从栈顶不断弹出节点,直至节点v被弹出

2)刚才弹出的所有节点与节点u一起构成一个点双连通分量

注意

<点双连通分量是一个极其容易误解的概念,它与“删除割点后图中剩余的连通块”的概念是不一样的>

献上模板题

【模板】点双连通分量 - 洛谷

AC代码:

#include 
using namespace std;
using LL = long long;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, idx = 0, bcc = 0;
    cin >> n >> m;
    vector> G(n + 1);
    vector> ans(n + 1);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    stack st;
    vector dfn(n + 1), low(n + 1);
    function tarjan = [&](int u, int fa) {
        low[u] = dfn[u] = ++idx;
        int son = 0;
        st.push(u);
        for (auto v : G[u]) {
            if (!dfn[v]) {
                son++;
                tarjan(v, u);
                low[u] = min(low[u], low[v]);
                if (low[v] >= dfn[u]) {
                    bcc++;
                    int z;
                    do {
                        z = st.top();
                        ans[bcc].push_back(z);
                        st.pop();
                    } while (z != v);
                    ans[bcc].push_back(u);
                }
            } else if (v != fa) {
                low[u] = min(low[u], dfn[v]);
            }
        }
        if (fa == 0 and son == 0) {
            ans[++bcc].push_back(u);
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, 0);
        }
    }
    cout << bcc << '\n';
    for (int i = 1; i <= bcc; i++) {
        cout << ans[i].size() << " ";
        for (auto j : ans[i]) {
            cout << j << " ";
        }
        cout << '\n';
    }
    return 0;
}

tarjan之求割边(桥)

除了割点还有一种问题是求割边(也称桥),即在一个无向图中删除某条边后,图不再连通

整体思路和求割点类似,只需要在求割点的代码里改一点点就可以,用链式前向星存图,这里用到一个链式前向星的技巧,有一条边直接连接i号点的前一个点可以直接用i^1得出,且cnt从2开始存才能满足成对变换(位运算特征)

链式前向星存图:

struct edge {
    int to, nxt;
}e[题目边数的两倍以上];
int cnt = 1, head[题目点数];
void add(int u, int v) {
    e[++cnt].to = v;
    e[cnt].nxt = head[u];
    head[u] = cnt;
}
main() {
    int u, v;
    cin >> u >> v;
    add(u, v);
    add(v, u);//无向图
}

tarjan求割边

void tarjan(int u, int fa) {
    dfn[u] = low[u] = ++idx;
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to;
        if (v == fa) {
            continue;
        }
        if (!dfn[v]) {
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if (low[v] > dfn[u]) {
                bridge++;//桥的个数++
                e[i].vis = e[i ^ 1].vis = true;//在存边的结构体里再维护一个vis,
                                                  //表示两点之间的边是否是桥
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

统计哪些是桥

vector> v;
for(int i = 0; i <= cnt; i += 2) {//cnt边的个数
    if (e[i].vis) {//判断是否是桥
        if (e[i].to < e[i ^ 1].to) {//点号小的在前
            v.push_back(make_pair(e[i].to, e[i ^ 1].to));
        }            
        else {
            v.push_back(make_pair(e[i ^ 1].to, e[i].to));
        }        
    }
}

洛谷竟然没有模板题QAQ,那只好直接过渡到求解边双连通分量了

tarjan之边双连通分量

概念

若一张无向连通图不存在桥(割边),则称它为“边双连通图”。

无向图的极大边双联通子图被称为“边双联通分量”,简记为“e-DCC”。

一张无向连通图是边双连通图,当且仅当任意一条边都包含在至少一个简单环中

做法

只需求出无向图中所有的桥,把桥都删除后,无向图会分成若干个连通块,每一个联通块都是一个“边双连通分量”。

在具体的程序实现中,一般先用 Tarjan 算法标记出所有的桥边。然后,再对整个无向图执行一次深度优先遍历(遍历的过程中不访问桥边),划分出每个连通块。

【模板】边双连通分量 - 洛谷

AC代码:

#include 
using namespace std;
using LL = long long;
struct edge {
    int to, nxt;
    bool vis;
};
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m, cnt = 1, idx = 0, dcc = 0;
    cin >> n >> m;
    vector head(n + 1);
    vector e(m * 2 + 2);
    function add = [&](int u, int v) {
        e[++cnt].to = v;
        e[cnt].nxt = head[u];
        e[cnt].vis = false;
        head[u] = cnt;
    };
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }
    vector dfn(n + 1), low(n + 1);
    function tarjan = [&](int u, int fa) {
        dfn[u] = low[u] = ++idx;
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to;
            if (!dfn[v]) {
                tarjan(v, i);
                low[u] = min(low[u], low[v]);
                if (low[v] > dfn[u]) {
                    e[i].vis = e[i ^ 1].vis = true;
                }
            } else if (i != (fa ^ 1)) {
                low[u] = min(low[u], dfn[v]);
            }
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, i);
        }
    }
    vector vis(n + 1);
    vector> ans(n + 1);
    function dfs = [&](int u) {
        vis[u] = true;
        if (u) {
            ans[dcc].push_back(u);
        }
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to;
            if (vis[v] or e[i].vis) {
                continue;
            }
            dfs(v);
        }
    };
    for (int i = 1; i <= n; i++) {
        if (!vis[i]) {
            dcc++;
            dfs(i);
        }
    }
    cout << dcc << '\n';
    for (int i = 1; i <= dcc; i++) {
        cout << ans[i].size() << " ";
        for (auto j : ans[i]) {
            cout << j << " ";
        }
        cout << '\n';
    }
    return 0;
}

完结!撒花!

你可能感兴趣的:(算法,图论,数据结构)