Tarjan 杂题选讲

写 CF 652E 的时候碰到了 DCC 相关内容于是恶补一发。

思维路径

  • 必经点(割点),必经边(割边)
  • 缩点后转化为树上问题或 DAG 上 DP
  • DCC、SCC 自身的性质

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

Tarjan 求 v-DCC 的基本思路就是利用一个充要条件:如果 u 是 dfs 树根节点,那么它有不少于两个儿子;否则存在一个 v 满足 low[v] >= dfn[u]

后面这个东西可以很直观地理解一下,如果你这棵子树内所有的点都没法在 u 以外的路径上走到 u 上面,那么它一定是割点。

还有一个小细节就是 dfn[v] != 0low[u] = std::min(low[u], dfn[v]),这里用 dfn[v] 更新是因为使用 low[v] 可能会跳到当前环上面去,而在有向图中使用 low[v] 也是对的。

const int MAXN = 2e4 + 10;
const int MAXM = 1e5 + 10;

int n, m;

struct Edge { int v, nxt; } edge[MAXM << 1]; int head[MAXN], cnt;

void addEdge(int u, int v) {
    edge[++cnt] = {v, head[u]}; head[u] = cnt;
}

int dfn[MAXN], low[MAXN], ts;
bool iscut[MAXN];
int fa[MAXN];

void Tarjan(int u) {
    dfn[u] = low[u] = ++ts;
    int childs = 0;
    for (int e = head[u]; e; e = edge[e].nxt) {
        int v = edge[e].v;
        if (!dfn[v]) {
            fa[v] = u;
            Tarjan(v);
            // 对是否为 dfs 树根进行讨论
            ++childs;
            low[u] = std::min(low[u], low[v]);
            if (fa[u] != u && low[v] >= dfn[u]) iscut[u] = true;
        } else if (v != fa[u]) low[u] = std::min(low[u], dfn[v]);
    }
    if (fa[u] == u && childs >= 2) iscut[u] = true;
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    n = read(); m = read();
    rep (i, 1, m) {
        int u = read(); int v = read();
        addEdge(u, v); addEdge(v, u);
    }
    rep (i, 1, n) {
        if (!dfn[i]) {
            fa[i] = i; Tarjan(i);
        }
    }
    std::vector<int> ans;
    rep (i, 1, n) {
        if (iscut[i]) ans.push_back(i);
    }
    printf("%d
", (int) ans.size());
    for (auto v : ans) printf("%d ", v);
    puts("");
    return 0;
}

A. CodeForces 652E Pursuit For Artifacts

将图里所有的 e-DCC 缩点,最终图会变成一棵树,每个点走出去了就不能再走进去了,每条边也只能走一次。

e-DCC 保证了这个分量内每两个点之间都存在至少两条不相交路径,也就是说,你走进去这个节点,如果里面有 artifact 那么一定能拿到并顺利走出来。

所以只需要看目标点之间的树上路径里有没有 artifact 就可以了。

const int MAXN = 3e5 + 10;

int n, m;

struct REdge { int u, v, w; } redges[MAXN];
struct Edge { int v, w, nxt; } edge[MAXN << 1]; int head[MAXN], cnt = 1;

void addEdge(int u, int v, int w) {
    edge[++cnt] = {v, w, head[u]}; head[u] = cnt;
}

int dfn[MAXN], low[MAXN], ts; bool iscut[MAXN << 1];

void Tarjan(int u, int fa) {
    dfn[u] = low[u] = ++ts;
    for (int e = head[u]; e; e = edge[e].nxt) {
        int v = edge[e].v;
        if (v == fa) continue;
        if (!dfn[v]) {
            Tarjan(v, u);
            low[u] = std::min(low[u], low[v]);
            if (low[v] > dfn[u]) {
                iscut[e] = iscut[e ^ 1] = true;
            }
        } low[u] = std::min(low[u], dfn[v]);
    }
}

int bel[MAXN], blkcnt;
bool has[MAXN];
void GetBlock(int u) {
    // DEBUG(blkcnt); DEBUG(u);
    bel[u] = blkcnt;
    for (int e = head[u]; e; e = edge[e].nxt) {
        int v = edge[e].v;
        if (iscut[e]) continue;
        has[blkcnt] |= edge[e].w;
        if (bel[v]) continue;
        GetBlock(v);
    }
}

Edge nedge[MAXN << 1]; int nhead[MAXN], ncnt;

void addNEdge(int u, int v, int w) {
    nedge[++ncnt] = {v, w, nhead[u]};
    nhead[u] = ncnt;
}

void rebuild() {
    rep (i, 1, m) {
        int u = redges[i].u, v = redges[i].v, w = redges[i].w;
        if (bel[u] != bel[v]) {
            addNEdge(bel[u], bel[v], w);
            addNEdge(bel[v], bel[u], w);
        }
    }
}

int dep[MAXN], faedge[MAXN];
void ndfs(int u, int fa) {
    dep[u] = dep[fa] + 1;
    for (int e = nhead[u]; e; e = nedge[e].nxt) {
        int v = nedge[e].v;
        if (v == fa) { faedge[u] = e; continue; }
        ndfs(v, u);
    }
}

bool Solve() {
    int x = bel[read()]; int y = bel[read()];
    if (dep[x] < dep[y]) std::swap(x, y);
    bool ans = false;
    while (dep[x] > dep[y]) {
        ans |= has[x];
        ans |= nedge[faedge[x]].w;
        x = nedge[faedge[x]].v;
    }
    if (x == y) return ans | has[x];
    while (x != y) {
        ans |= has[y];
        ans |= nedge[faedge[y]].w;
        y = nedge[faedge[y]].v;
        ans |= has[x];
        ans |= nedge[faedge[x]].w;
        x = nedge[faedge[x]].v; 
    }
    return ans | has[x];
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    n = read(); m = read();
    rep (i, 1, m) {
        int u = read(); int v = read(); int w = read();
        redges[i] = {u, v, w};
        addEdge(u, v, w); addEdge(v, u, w);
    }
    Tarjan(1, 0);
    rep (i, 1, n) if (!bel[i]) { ++blkcnt; GetBlock(i); }
    rebuild();
    // rep (i, 1, n) printf("(%d -> %d) ", i, bel[i]);
    // puts("");
    ndfs(1, 0);
    printf("%s
", Solve() ? "YES" : "NO");
    return 0;
}

B. 洛谷P2746 [USACO5.3]校园网Network of Schools

SCC 缩点板子题。

任务 1 就是入度为 0 的点的个数,任务 2 就是入度为 0 和出度为 0 点个数的较大值。

const int MAXN = 100 + 10;

int n;

std::vector<int> G[MAXN];

int dfn[MAXN], low[MAXN], ts, stk[MAXN], top, instk[MAXN];
int bel[MAXN], blkcnt;
void Tarjan(int u) {
    dfn[u] = low[u] = ++ts; stk[++top] = u; instk[u] = true;
    forall (G[u], i) {
        int v = G[u][i];
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = std::min(low[u], low[v]);
        } else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) {
        ++blkcnt;
        do {
            bel[stk[top]] = blkcnt; instk[stk[top]] = false; --top;
        } while (stk[top + 1] != u);
    }
}

int ind[MAXN], oud[MAXN];

void rebuild() {
    rep (u, 1, n) {
        for (auto v : G[u]) {
            if (bel[u] != bel[v]) {
                ++ind[bel[v]]; ++oud[bel[u]];
            }
        }
    }
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    n = read();
    rep (i, 1, n) {
        int x = read();
        while (x) {
            G[i].push_back(x); x = read();
        }
    }
    rep (i, 1, n) if (!dfn[i]) Tarjan(i);
    rebuild();
    int ans1 = 0; rep (i, 1, blkcnt) ans1 += (ind[i] == 0);
    int ans2 = 0; rep (i, 1, blkcnt) ans2 += (oud[i] == 0);
    printf("%d
%d
", ans1, blkcnt == 1 ? 0 : std::max(ans1, ans2));
    return 0;
}

C. [国家集训队]稳定婚姻

好离谱一题

声明:以下场景皆为假设,没有任何对女性的不尊重,本题也并不假定 (B)(G) 为人类性别。

首先注意到戴绿帽这个过程是:(B ightarrow G ightarrow B ightarrow G dots) 或者 (G ightarrow B ightarrow G ightarrow B dots),也就是 (B, G) 交替进行,最终一定会形成一个环,否则最后一个人会落单。

然后继续仔细分析戴绿帽的过程:

比如说 (G_7) 出轨去找 (B_6),此时 (G_6) 会去找 (B_5)(G_5) 会去找 (B_4)……一直到 (G_1),如果能找到 (B_7),那么这 14 个人又形成了 7 对,也就是 (B_7 longleftrightarrow G_7) 是不安全的。

这个过程中,每一对情侣 (G_7 ightarrow B_6)(G_6 ightarrow B_5)……都是从 (G) 导向 (B),上一次出轨导致拆散的夫妻 (B_6 ightarrow G_6)(B_5 ightarrow G_5) 都是从 (B) 导向 (G),最终形成了一个环。

所以考虑对于每一对夫妻连有向边 (B ightarrow G),对于每一对情侣连有向边 (B leftarrow G),之后 Tarjan 缩环,如果一对夫妻属于一个环就是不安全的。

关于本题为什么不能建无向图然后跑 e-DCC,可以看看 这个

const int MAXN = 8000 + 10;

int n, m;
std::vector<int> G[MAXN];

std::map<std::string, int> idx; int cnt;

int dfn[MAXN], low[MAXN], ts, stk[MAXN], top, instk[MAXN];
int bel[MAXN], blkcnt; 

void Tarjan(int u) {
    dfn[u] = low[u] = ++ts; stk[++top] = u; instk[u] = true;
    forall (G[u], i) {
        int v = G[u][i];
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = std::min(low[u], low[v]);
        } else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) {
        ++blkcnt;
        do {
            bel[stk[top]] = blkcnt;
            instk[stk[top]] = false; --top;
        } while (stk[top + 1] != u);
    }
}

struct E {int u, v;} es[MAXN];

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    cin >> n;
    rep (i, 1, n) {
        std::string bb, gg; cin >> bb >> gg;
        idx[bb] = ++cnt; idx[gg] = ++cnt;
        es[i] = {cnt - 1, cnt};
        G[idx[bb]].push_back(idx[gg]);
    }
    cin >> m;
    rep (i, 1, m) {
        std::string bb, gg; cin >> bb >> gg;
        G[idx[gg]].push_back(idx[bb]);
    }
    rep (i, 1, cnt) if (!dfn[i]) Tarjan(i);
    rep (i, 1, n) {
        int u = es[i].u, v = es[i].v;
        cout << ((bel[u] == bel[v]) ? "Unsafe" : "Safe") << endl;
    }
    return 0;
}

D. [ZJOI2007]最大半连通子图

半联通分量我们不大会求,但是我们会求强联通分量,显然一个强联通分量一定是半联通分量。所以先把原图 SCC 缩点。

考虑两个相邻的强联通分量,发现它们合起来也是一个半联通分量,因为前一个里面所有点可以到达后一个里面所有点。

所以题目转化成了 DAG 上最长链。

const int MAXN = 1e5 + 10;

int n, m, x;
std::vector<int> G[MAXN];

int dfn[MAXN], low[MAXN], ts, stk[MAXN], top, instk[MAXN];
int bel[MAXN], blk, siz[MAXN];
void Tarjan(int u) {
    dfn[u] = low[u] = ++ts; stk[++top] = u; instk[u] = true;
    forall (G[u], i) {
        int v = G[u][i];
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = std::min(low[u], low[v]);
        } else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
    }
    if (low[u] == dfn[u]) {
        ++blk;
        do {
            ++siz[blk];
            bel[stk[top]] = blk; instk[stk[top]] = false;
            --top;
        } while (stk[top + 1] != u);
    }
}

int nn;
std::set<int> F[MAXN]; int ww[MAXN];
int ind[MAXN];

void rebuild() {
    nn = blk;
    rep (u, 1, n) {
        for (auto v : G[u]) {
            if (bel[u] != bel[v]) {
                if (!F[bel[u]].count(bel[v])) ++ind[bel[v]];
                F[bel[u]].insert(bel[v]);
            }
        }
    }
    rep (i, 1, nn) ww[i] = siz[i];
}

int dp[MAXN], cnt[MAXN];
void DP() {
    std::queue<int> q;
    for (int i = 1; i <= nn; ++i) if (!ind[i]) {
        dp[i] = ww[i]; cnt[i] = 1 % x;
        q.push(i);
    }
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (auto v : F[u]) {
            // dp[v] = std::max(dp[v], dp[u] + ww[v]);
            if (dp[u] + ww[v] > dp[v]) {
                dp[v] = dp[u] + ww[v];
                cnt[v] = cnt[u] % x;
            } else if (dp[u] + ww[v] == dp[v]) {
                (cnt[v] += cnt[u]) %= x;
            }
            --ind[v];
            if (!ind[v]) q.push(v);
        }
    }
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    n = read(); m = read(); x = read();
    rep (i, 1, m) {
        int u = read(); int v = read();
        G[u].push_back(v);
    }
    rep (i, 1, n) if (!dfn[i]) Tarjan(i);
    rebuild(); DP();
    int maxdp = 0, maxcnt = 0;
    for (int i = 1; i <= nn; ++i) {
        if (maxdp < dp[i]) {
            maxdp = dp[i]; maxcnt = cnt[i];
        } else if (maxdp == dp[i]) {
            (maxcnt += cnt[i]) %= x;
        }
    }
    printf("%d
%d
", maxdp, maxcnt);
    return 0;
}

E. [HNOI2012]矿场搭建

如果某个点到逃生通道的必经点塌了就不好玩了。所以这题肯定和“必经点”——割点有关。于是先一遍 Tarjan 求出所有割点。

之后对每个 DCC 分类讨论:

  • 如果这个 DCC 有不少于两个割点,那么这个 DCC 里不需要建任何逃生通道,如果一个割点塌了,那么就可以从其他割点逃出去;
  • 如果这个 DCC 只有一个割点,也就是割点上挂了个联通块,那么这个 DCC 里需要建一个逃生通道,如果割点塌了就要从自己的通道出去;
  • 如果这个 DCC 没有割点,也就是一整个连通分量,需要建两个逃生通道互相备份,防止另一个塌了。

方案数就是从一些点中选两个 / 选一个的方案数。总方案数就是每个 DCC 的方案数乘起来。

const int MAXN = 1000 + 10;

int n, m;
std::vector<int> G[MAXN];

int dfn[MAXN], low[MAXN], ts;
bool iscut[MAXN];
int fa[MAXN];

void Tarjan(int u) {
    dfn[u] = low[u] = ++ts;
    int childs = 0;
    for (auto v : G[u]) {
        if (!dfn[v]) {
            fa[v] = u;
            Tarjan(v);
            // 对是否为 dfs 树根进行讨论
            ++childs;
            low[u] = std::min(low[u], low[v]);
            if (fa[u] != u && low[v] >= dfn[u]) iscut[u] = true;
        } else if (v != fa[u]) low[u] = std::min(low[u], dfn[v]);
    }
    if (fa[u] == u && childs >= 2) iscut[u] = true;
}

int blk, col[MAXN];
int cuts, siz;
void dfs(int u) {
    col[u] = blk;
    ++siz;
    for (auto v : G[u]) {
        if (col[v] != blk && iscut[v]) {
            col[v] = blk; ++cuts;
        }
        if (!col[v] && !iscut[v]) {
            dfs(v);
        }
    }
}

void cleanup() {
    rep (i, 1, n) {
        G[i].clear();
        dfn[i] = low[i] = iscut[i] = col[i] = fa[i] = 0;
    }
    n = 0; m = 0; 
    blk = ts = 0;
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int cas = 0;
    while (scanf("%d", &m) != EOF) {
        if (m == 0) break;
        ++cas;
        for (int i = 1; i <= m; ++i) {
            int u, v; scanf("%d %d", &u, &v);
            G[u].push_back(v); G[v].push_back(u);
            n = std::max({n, u, v});
        }
        rep (i, 1, n) if (!dfn[i]) { fa[i] = i; Tarjan(i); }
        unsigned long long int ans = 0, cnt = 1;
        rep (i, 1, n) {
            cuts = siz = 0;
            if (col[i] || iscut[i]) continue;
            ++blk;
            dfs(i);
            if (cuts == 0) ans += 2, cnt *= 1ull * (siz - 1) * siz / 2;
            else if (cuts == 1) ans += 1, cnt *= 1ull * siz;
        }
        printf("Case %d: %llu %llu
", cas, ans, cnt);
        cleanup();
    }
    return 0;
}

F. [ZJOI2004]嗅探器

必经点。显然答案集合是图中的某些(不一定是全部)割点,还要保证这个割点在两个中心服务器 (a, b) 的路径上。

所以一种方法是缩点之后在树上跑一遍 DFS,但是我写挂了。

另一种方法是考虑从一个中心服务器 (a) 开始 Tarjan,在每一个割点 (u) 出现时判断它是否符合要求,“符合要求”意味着另一个中心服务器 (b) 在 DFS 树上应该在 (u) 的子树中。

考虑一个割点 (u) 在遍历到 ((u, v)) 这条边时被设置为割点,那么通过 dfn,我们可以判断 (b) 是否在 (u) 的子树中:

  • dfn[u] > dfn[b],这种情况说明 (b) 可能是 (u) 的祖先或者在 (u) 的子树外,或者压根就没被遍历到;
  • dfn[u] < dfn[b] < dfn[v],这种情况说明 (b)(v) 是兄弟子树关系,然而无法判断 (b)(a) 是否不在一个 DCC;
  • dfn[b] > dfn[v],这种情况 (b) 一定在 (u) 的子树中,因为回溯到 (v) 点时,所有 dfn 大于它的一定在它子树里面。

所以就把 Tarjan 板子改改就过了。当然要注意本题根节点是不能作为割点的。

const int MAXN = 2e5 + 10;

int n;
int ss, tt;
std::vector<int> G[MAXN];

int dfn[MAXN], low[MAXN], ts;
int fa[MAXN];
bool iscut[MAXN];

int ans = (1 << 30);

void Tarjan(int u) {
    // no need to check the root
    dfn[u] = low[u] = ++ts;
    int childcnt = 0;
    for (auto v : G[u]) {
        if (!dfn[v]) {
            ++childcnt;
            fa[v] = u;
            Tarjan(v);
            low[u] = std::min(low[u], low[v]);
            if (dfn[tt] >= dfn[v]) {
                // if (u == fa[u] && childcnt >= 2) ans = std::min(ans, u);
                if (u != fa[u] && low[v] >= dfn[u]) ans = std::min(ans, u);
            }
        } else if (v != fa[u]) low[u] = std::min(low[u], dfn[v]);
    }
}

int main() {
    std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    n = read();
    while (true) {
        int u = read(); int v = read();
        if (u == 0 && v == 0) break;
        G[u].push_back(v); G[v].push_back(u);
    }
    ss = read(); tt = read();

    fa[ss] = ss; Tarjan(ss);

    if (ans == (1 << 30)) puts("No solution");
    else printf("%d
", ans);
    return 0;
}
原文地址:https://www.cnblogs.com/handwer/p/15497881.html