To boldly go where no one has gone before.

【学习笔记】Tarjan

2019-02-23 15:42:27


$Tarjan$算法的核心思路是:对于每一个点,维护一个时间戳$dfn$以及一个最早可回溯祖先$low$,这两个东西的初始化依靠dfs算法实现。$dfn$表示在dfs中被搜索到的时间戳,而$low$表示的是通过该点的子边所能到达的最早祖先。同时维护一个栈,每次dfs时将点入栈,以此查询父子关系。有以下三种算法:

1) 如果发现一个点对应的$low$=$dfn$,那么在当前已入栈的点及其子边所构成的子图中,以这个点为始点的子图就组成了一个强连通分量。

2) 对于一条边$(u,v)$,对应的$low[v]\ge dfn[u]$;或者$u$为根节点,且拥有2个以上的子节点,那么$u$是一个割点。

3) 对于一条边$(u,v)$,对应的$low[v]>dfn[u]$,并且$(u,v)$不是重边,那么$(u,v)$是一个桥。

割点

模板P3388

最坑的地方在于,对于边$(u,v)$,如果$v$已经被访问过了,在更新$low[u]$的时候不再用$low[u]=min(low[u], low[v])$,而用的是$$ low[u] = min(low[u],dfn[v])$$ 具体原因是$low[v]$可能会指向$v$所能访问到的另外一个祖先,如果继续用$low[v]$更新,就可能跳出当前的访问路径,跑到其他地方去了,这显然对于割点的判断是有影响的。因为就算$v$存在于其他强连通分量中,只要$(u,v)$满足$low[v]\ge dfn[u]$,去掉$u$之后依然能够将图断开。桥也同理,因为依赖的不是最后结果而是过程中对$low$和$dfn$的比较。因此要记住这个坑点。

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 100010;

struct EDGE {
    int to, next;
} edge[MAXN*2];

int cnt = 0, n, m, id = 0, tot = 0, x, y;
int last[MAXN], low[MAXN], dfn[MAXN];
bool cut[MAXN];

void build_edge(int u, int v) {
    edge[++cnt].to = v;
    edge[cnt].next = last[u];
    last[u] = cnt;
    return;
}

void Tarjan(int o, int fa) {
    dfn[o] = low[o] = ++id;
    int c_cnt = 0;
    for(int i=last[o]; i; i=edge[i].next) {
        int p = edge[i].to;
        if(!dfn[p]) {
            Tarjan(p, fa);
            low[o] = min(low[o], low[p]);
            if(o!=fa && low[p]>=dfn[o]) {
                cut[o] = true;
            }
            if(o == fa)
                c_cnt++;
        } else
        low[o] = min(low[o], dfn[p]);
    }
    if(o==fa && c_cnt>=2)
        cut[o] = true;
    return;
}

int main() {
    memset(cut, 0, sizeof(cut));
    cin >> n >> m;
    for(int i=1; i<=m; i++) {
        cin >> x >> y;
        build_edge(x, y);
        build_edge(y, x);
    }
    for(int i=1; i<=n; i++)
        if(!dfn[i]) Tarjan(i, i);
    for(int i=1; i<=n; i++)
        if(cut[i]) tot++;
    cout << tot << endl;
    for(int i=1; i<=n; i++)
        if(cut[i]) cout << i << " ";
    return 0;
}

桥其实差不多,由于没有模板所以就不打标程上来了。代码没测试,我也不知是不是对的,大概看看就好。要注意的是特判重边,因为对于一个桥$(u,v)$,当访问$(v,u)$的时候如果不特判的话就会直接把这个无向边认作环,实际上这个切掉这个无向边依然能把图一分为二。

void Tarjan(int u, int fa) {
    dfn[u] = low[u] = ++id;
    for(int i=last[u]; i; i=edge[i].next) {
        int v = edge[i].to;
        if(v == fa) continue;
        if(!dfn[v]) {
            Tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v] > dfn[u])
                cut[i] = true;
        } else if(v != fa) // 或者改成非反向边的判定条件
            low[u] = min(low[u], dfn[v]); //坑
    }
    return;
}

缩点

一道有缩点的模板P3387

这道题就是$Tarjan+DAGdp$解决,因为缩点之后就变成无环图了。$DAGdp$之前有写到过就不说了。 缩点就是判断强连通分量,把它们全部用一个点来替代,然后建立新图。具体可以用染色实现。

缩点的时候只要遇到$low[u]=dfn[u]$,那么就从之前的栈里头一直取点,一直到把$u$给取出来为止,然后把它们全部染色。另外,在更新$low$的时候$else$后跟的$low[u]=min(low[u], low[v])$可以换成$low[u]=min(low[u], dfn[v])$,因为两者在结果上是等价的,只是前者的适用范围仅有强连通分量,而不能用于判断割点和桥而已。所以以后还是直接打后者算了,免得到时候搞忘坑点。

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 10010;

struct EDGE {
    int from, to, next;
} edge[MAXN*10], _edge[MAXN*10];

int last[MAXN], _last[MAXN], cnt = 0;
int dfn[MAXN], low[MAXN], id = 0;
int s[MAXN], top = 0;
bool ins[MAXN];
int n, m, x, y, p[MAXN], clr[MAXN], f[MAXN], in[MAXN];

void build_edge(int u, int v) {
    edge[++cnt].from = u;
    edge[cnt].to = v;
    edge[cnt].next = last[u];
    last[u] = cnt;
    return;
}

void Tarjan(int u) {
    dfn[u] = low[u] = ++id;
    s[++top] = u;
    ins[u] = true;
    for(int i=last[u]; i; i=edge[i].next) {
        int v = edge[i].to;
        if(!dfn[v]) {
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        }else if(ins[v])
            low[u] = min(low[u], dfn[v]);
    }
    if(low[u] == dfn[u]) {
        int v;
        while(v = s[top--]) {
            clr[v] = u;
            ins[v] = false;
            if(u == v) break;
            p[u] += p[v];
        }
    }
    return;
}

void toposort() {
    queue<int> q;
    for(int i=1; i<=n; i++)
        if(i==clr[i] && !in[i]) {
            q.push(i);
            f[i] = p[i];
        }
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        for(int i=_last[u]; i; i=_edge[i].next) {
            int v = _edge[i].to;
            f[v] = max(f[v], f[u]+p[v]);
            in[v]--;
            if(!in[v])
                q.push(v);
        }
    }
    return;
}

int main() {
    cin >> n >> m;
    for(int i=1; i<=n; i++)
        cin >> p[i];
    for(int i=1; i<=m; i++) {
        cin >> x >> y;
        build_edge(x, y);
    }
    cnt = 0;
    for(int i=1; i<=n; i++)
        if(!dfn[i]) Tarjan(i);
    for(int i=1; i<=m; i++) {
        int u = clr[edge[i].from], v = clr[edge[i].to];
        if(u != v) {
            _edge[++cnt].to = v;
            _edge[cnt].next = _last[u];
            _last[u] = cnt;
            in[v]++;
        }
    }
    toposort();
    int ans = 0;
    for(int i=1; i<=n; i++)
        ans = max(ans, f[i]);
    cout << ans;
    return 0;
}

不要再忘记$Tarjan$了,重新从头学一遍太痛苦了哇!