zoukankan      html  css  js  c++  java
  • 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;
    }
    
  • 相关阅读:
    SQL语句面试题目:一般查询和高级子查询
    几种常见的排序算法分析学习
    60秒倒计时
    JS倒计时
    sonarqube linux安装总结,集成jenkins
    spring boot打成可执行jar
    Spring Cloud服务间调用鉴权
    Spring Cloud Hystrix熔断器隔离方案
    Spring boot 集成Swagger
    Spring boot 集成Swagger
  • 原文地址:https://www.cnblogs.com/handwer/p/15497881.html
Copyright © 2011-2022 走看看