Loading

[POJ 3694] Network

算法

暴力

容易想到的做法是, 对于每次加边, 跑一次 \(\rm{Tarjan}\) 求割点, 但是每次 \(\rm{Tarjan}\) 的复杂度是 \(\mathcal{O} (n + m)\) , 总的时间复杂度可以卡到 \(\mathcal{O} (Qn + Qm)\) , 显然不能用

正解

暴力的慢是显然的, 主要原因是每次加边对于原图的改变并不大, 完全不应该重跑一遍 \(\rm{Tarjan}\)

考虑优化

显然, 原图将边双联通分量缩点之后将变成一颗树

我们考虑在树上处理每一次询问

问题转化成,

对于一颗树, 有 \(Q\) 次加边, 每次加边之后, 输出缩点之后树上边的个数


显然的, 加边之后产生的环上割边都会被消除, 问题转化成如何计算这个环上有多少点

观察到环的形式大概是 \(u \to \rm{LCA}(u, v) \to v \to u\)

考虑求 \(\rm{LCA}\) , 顺便将路过的点标记上

具体的, 将每个点属于的 \(\rm{SCC}\) 用并查集更新, 并标记割边不存在

代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>

const int MAXN = 3e5 + 20;
const int MAXM = 3e5 + 20;

int N, M, Q;
int CutNum; // 桥梁个数

class Graph_Class
{
private:

public:
    struct node
    {
        int to, w;
        int next;
    } Edge[MAXM << 2];
    int Edge_Cnt;
    int head[MAXN << 1];
    void head_init() { for (int i = 0; i <= N + 1; i++) head[i] = -1; }
    void init() {
        head_init();
        Edge_Cnt = 0;
        memset(Edge, 0, sizeof(Edge));
    }

    inline void addedge(int u, int v) {
        Edge[++Edge_Cnt].to = v, Edge[Edge_Cnt].w = 0;
        Edge[Edge_Cnt].next = head[u];
        head[u] = Edge_Cnt;
        return;
    }
    int fa[MAXN << 1], dep[MAXN << 1];
} Graph1, Graph2;

class Sol_Class
{
private:
    int low[MAXN << 1], num;

    std::basic_string<int> SCC[MAXN << 1];

    void init() {
        memset(low, 0, sizeof(low));
        num = 0;
        for(int i = 1; i <= N; i++) SCC[i].clear();
        return;
    }

    /*找到对应的 SCC*/
    void tarjan(int Now, int fa)
    {
        low[Now] = ++num;
        for(int i = Graph1.head[Now]; ~i; i = Graph1.Edge[i].next) {
            int NowTo = Graph1.Edge[i].to;

            if (NowTo == fa) continue;
            if (!low[NowTo]) tarjan(NowTo, Now);
            low[Now] = std::min(low[Now], low[NowTo]);
        }

        SCC[low[Now]] += Now;
    }

    struct DSU_Struct
    {
        int Bel[MAXN << 1];
        void init() { for (int i = 1; i <= N; i++) Bel[i] = i; }

        int find(int x) { return Bel[x] = ((x == Bel[x]) ? x : find(Bel[x])); }
        inline void merge(int u, int v) {
            int fa_u = find(u), fa_v = find(v);
            if (fa_u == fa_v) return ;
            Bel[fa_v] = fa_u;
        }
    } ;

    /*计算 Graph2 树的父亲节点及深度*/
    void dfs1(int Now, int fa, int depth) {
        Graph2.dep[Now] = depth;
        Graph2.fa[Now] = fa;

        for(int i = Graph2.head[Now]; ~i; i = Graph2.Edge[i].next) {
            int NowTo = Graph2.Edge[i].to;

            if(NowTo == fa) continue;
            dfs1(NowTo, Now, depth + 1);
        }
    }

    /*在之前记录的树的信息的帮助下, 找到最近公共祖先, 这里只需要使用朴素算法即可*/
    int FindLCA(int u, int v)
    {
        while (Graph2.dep[u] != Graph2.dep[v])
        {
            if (Graph2.dep[u] > Graph2.dep[v]) {
                u = Graph2.fa[u];
            }else if (Graph2.dep[u] < Graph2.dep[v]) {
                v = Graph2.fa[v];
            }else {
                u = Graph2.fa[u];
                v = Graph2.fa[v];
            }

            u = DSU.find(u), v = DSU.find(v);
        }
        return u;
    }

public:
    DSU_Struct DSU;

    /*计算 SCC 个数 & 建立新图*/
    void BuildSCC()
    {
        CutNum = 0;

        init();
        tarjan(1, -1);

        DSU.init();
        for (int i = 1; i <= N; i++) {
            if (SCC[i].size()) {
                for (int j = 1; j < SCC[i].size(); j++) {
                    DSU.merge(SCC[i][j - 1], SCC[i][j]);
                }
                CutNum++;
            }
        }
        CutNum--;

        Graph2.init();
        for (int i = 1; i <= N; i++)
            for (int j = Graph1.head[i]; ~j; j = Graph1.Edge[j].next) {
                int NowTo = Graph1.Edge[j].to;
                int fa_i = DSU.find(i), fa_j = DSU.find(NowTo);
                if (fa_i == fa_j) continue;

                Graph2.addedge(fa_i, fa_j);
                Graph2.addedge(fa_j, fa_i);
            }

        dfs1(DSU.find(1), -1, 1); // 以 1 在 Graph2 中的对应点为根节点记录数据
    }

    /*计算 u, v 连边之后桥梁的个数*/
    void solve(int u, int v)
    {
        int LCA = FindLCA(u, v);

        while(u != LCA) {
            DSU.merge(LCA, u);
            u = DSU.find(Graph2.fa[u]);
            CutNum--;
        }
        while(v != LCA) {
            DSU.merge(LCA, v);
            v = DSU.find(Graph2.fa[v]);
            CutNum--;
        }


    }
} Sol;

int main()
{
    int Cnt = 0;
    while(++Cnt)
    {
        scanf("%d %d", &N, &M);
        if (N == 0 && M == 0) break;
        printf("Case %d:\n", Cnt);

        Graph1.init();
        for (int i = 1; i <= M; i++) {
            int u, v;
            scanf("%d %d", &u, &v);
            Graph1.addedge(u, v), Graph1.addedge(v, u);
        }

        Sol.BuildSCC();

        scanf("%d", &Q);
        while (Q--) {
            int u, v;
            scanf("%d %d", &u, &v);
            int fa_u = Sol.DSU.find(u), fa_v = Sol.DSU.find(v);
            if (fa_u == fa_v) printf("%d\n", CutNum);
            else {
                Sol.solve(fa_u, fa_v);
                printf("%d\n", CutNum);
            }
        }

        printf("\n");
    }

    return 0;
}

代码实现细节较多, 这里写下我代码时发现的一些问题:

  1. \(\rm{tarjan}\)\(\rm{SCC}\) 注意不用 \(dfn\) 数组
  2. 关于 \(\rm{SCC}\) 缩点 : 缩完之后先建立新图, 后边的更改每次只会将一个环变成一个 \(\rm{SCC}\) , 不需要建立新图
  3. 关于如何将一个环缩成一个点 : 先找 \(\rm{LCA}\) , 在向上直到 \(\rm{LCA}\) 的过程中 , 将路过的点属于的 \(SCC\) 全部标记到 \(\rm{LCA}\) 属于的 \(\rm{SCC}\)
  4. 关于 \(dep\) 数组 : 在后面, \(dep\) 数组可能不连续, 但是 \(\rm{LCA}\) 只需要一个大小的关系即可, 这也导致了这份代码中求 \(\rm{LCA}\) 的代码片段略有不同

润回来复习

建立 \(\rm{SCC}\)

这里我们直接按照意义真正建立一颗树

\(\rm{SCC}\) 树上进行处理

你发现每次重跑 \(\rm{tarjan}\) 很糖, 所以考虑就在原来的树上处理新的 \(\rm{SCC}\)
你发现加上边 \((u, v)\) 之后, 会多一个 \(u \to \textrm{LCA} (u, v) \to v \to u\) 的环, 考虑怎么处理现在的 \(\rm{SCC}\)

容易发现的是, 最终环上的点都会进入一个 \(\rm{SCC}\) , 但是我们不可能再想最早一样, 真正的建立一颗树, 只能在现在的树上打标记以建立新树

具体怎么做, 我们考虑使用并查集处理每一个 \(\rm{SCC}\) , 钦定一个实点表示这个 \(\rm{SCC}\) 的信息, 其他都是虚点, 并查集就维护这个 \(\rm{SCC}\) 对应的实点

实现方法 \(1\)

\(u, v\) 往上跳到 \(\rm{LCA}\) 的时候一定可以遍历完环上的点, 这个过程中用并查集标记点, 然后每次处理遇到一个点就并查集找他的实点处理
任意两点在 \(\rm{LCA}\) 处合并, 在这题中可以保持信息的有效性

实现方法 \(2\)

每次跳 \(\rm{LCA}\) 的时候维护一个点, 然后把虚点插进去, 这样子更有一般性, 时间复杂度要差些
也要用并查集标记每个 \(\rm{SCC}\) 对应的实点, 注意实点还有可能变成另一个点的虚点

总结

暴力 \(\to\) 优化

善于分析问题本质

这样一类缩点题, 通常只需要建立一次新图, 后面只需在图上做处理即可
更一般的, 在新图上做处理就只能打标记而不能再建立新图了
打标记一般就用并查集, 注意随时找实点即可

树有重边一定要用边判断不要用 \(fa\) , 警钟长鸣

posted @ 2024-11-18 17:27  Yorg  阅读(7)  评论(0)    收藏  举报