首页 > 其他分享 >【W的AC企划 - 第八期】tarjan缩点

【W的AC企划 - 第八期】tarjan缩点

时间:2023-08-16 21:33:59浏览次数:39  
标签:缩点 AC ver int cnt DCC tarjan dfn low

往期浏览

第一期 - 博弈论(game)

第二期 - 前缀和

第三期 - 二分算法

第四期 - 莫队算法

第五期 - 线段树

第六期 - 位运算 (Bitmasks)

第七期 - 树上分治

第八期 - Tarjan缩点

第九期(拟) - 树上启发式合并

讲解

(有向图)强连通分量缩点

概念

强连通分量缩点后的图称为 SCC。

有两种方式会导致形成 SCC,其一是存在一条后向边,直接指向某个祖先节点;其二是存在一条横叉边,横叉边指向某个祖先节点。

求解强连通分量:该算法主要依赖于 dfs,定义两个数组,time 数组代表遍历到该点的时间戳, upper 数组代表从该点开始走能走到的最小时间戳。对于某一点 \(x\) ,如果满足 time[x] == upper[x] ,说明这个点位于一个强连通分量的最上层,这时可以借助栈 S 来获得 \(x\) 所在 SCC 的全部点的编号:一直到 \(x\) 出栈为止,这些点都属于同一个 SCC。

强连通分量缩点操作过程:对于任意的边 \((x_i,y_i)\) ,如果它们不属于同一个强连通分量,那么就建立一条新边。

最终我们以 \(\mathcal O (N + M)\) 的复杂度完成上述全部操作。

个人封装

struct SCC {
    int n;
    vector<vector<int>> ver;
    vector<int> dfn, low, col, S;
    int now, cnt;

    SCC(int n) : n(n) {
        ver.assign(n + 1, {});
        dfn.resize(n + 1, -1);
        low.resize(n + 1);
        col.assign(n + 1, -1);
        S.clear();
        now = cnt = 0;
    }
    void add(int x, int y) {
        ver[x].push_back(y);
    }
    void tarjan(int x) {
        dfn[x] = low[x] = now++;
        S.push_back(x);
        for (auto y : ver[x]) {
            if (dfn[y] == -1) {
                tarjan(y);
                low[x] = min(low[x], low[y]);
            } else if (col[y] == -1) {
                low[x] = min(low[x], dfn[y]);
            }
        }
        if (dfn[x] == low[x]) {
            int pre;
            cnt++;
            do {
                pre = S.back();
                col[pre] = cnt;
                S.pop_back();
            } while (pre != x);
        }
    }
    pair<int, vector<vector<int>>> rebuild() { // [新图的顶点数量, 新图]
        work();
        vector<vector<int>> adj(cnt + 1);
        for (int i = 1; i <= n; i++) {
            for (auto j : ver[i]) {
                int x = col[i], y = col[j];
                if (x != y) {
                    adj[x].push_back(y);
                }
            }
        }
        return {cnt, adj};
    }
    void work() {
        for (int i = 1; i <= n; i++) { // 避免图不连通
            if (dfn[i] == -1) {
                tarjan(i);
            }
        }
    }
};

(无向图)割边缩点

概念

割边缩点后的图称为 E-DCC。

较有向图中 SCC 的形成更简单,只有一种方式会形成 E-DCC,即不存在一条边,直接指向某个祖先节点。

求解割边:我们同样维护 timeupper 两个时间戳数组。对于某一条边 \((x-y)\) ,如果满足 time[x] < upper[y] ,说明 \(y\) 不能到达 \(x\) 的任何一个祖先节点,这条边就是割边。然而,在处理重边问题时,借助 \(\tt vector\) 的方法无法对之进行处理(例如同时存在 \((u,v)\) 和 \((v,u)\) 两条边时,链式前向星可以借助 h 数组直接区分这两个 u 的不同,进而判断这条边不是割边,但是在 \(\tt vector\) 数组中这两个 u 是相同的),故求解割边一般都采用链式前向星建图法

求解边双:如果满足 time[z] == upper[z] ,\(z\) 就位于一个边双的最上层,同样可以借助栈 S 来获得 \(z\) 所在边双的全部点的编号:一直到 \(z\) 出栈为止,这些点都属于同一个 E-DCC 。

边双缩点操作过程:旧图中的每一条割边即为新图中的一条边,获取割边两端点颜色后直接连接即可。下方给出的链式前向星方法是在求解割边的基础上顺便将边双缩点,如果不需要求解割边而只需要将边双缩点,那么直接套用 SCC 缩点的做法即可。

个人封装

struct E_DCC {
    int n;
    vector<int> h, ver, ne;
    vector<int> dfn, low, col, S;
    int now, cnt, tot;
    vector<bool> bridge; // 记录是否是割边

    E_DCC(int n, int m) : n(n) {
        m *= 2; // 注意链式前向星边的数量翻倍
        ver.resize(m + 1);
        ne.resize(m + 1);
        bridge.resize(m + 1);
        
        h.resize(n + 1, -1);
        dfn.resize(n + 1);
        low.resize(n + 1);
        col.resize(n + 1);
        S.clear();
        tot = cnt = now = 0;
    }
    void add(int x, int y) { // 注意,这里的编号从 0 开始
        ver[tot] = y, ne[tot] = h[x], h[x] = tot++;
        ver[tot] = x, ne[tot] = h[y], h[y] = tot++;
    }
    void tarjan(int x, int fa) { // 这里是缩边双,不是缩点,不相同
        dfn[x] = low[x] = ++now;
        S.push_back(x);
        for (int i = h[x]; ~i; i = ne[i]) {
            int y = ver[i];
            if (!dfn[y]) {
                tarjan(y, i); // 这里储存的是父亲边的编号
                low[x] = min(low[x], low[y]);
                // y 不能到达 x 的任何一个祖先节点,(x - y) 即为一条割边
                // 但是在这里,我们不直接储存 (x - y) 这条边,而是储存边的编号
                // 这样做是为了处理重边的情况(点可能相同,但是边的编号绝对不相同)
                if (dfn[x] < low[y]) {
                    bridge[i] = bridge[i ^ 1] = true;
                }
            } else if (i != (fa ^ 1)) { // 这里同样的,使用边的编号来处理重边情况
                low[x] = min(low[x], dfn[y]);
            }
        }
        if (dfn[x] == low[x]) {
            int pre = 0;
            cnt++;
            do {
                pre = S.back();
                S.pop_back();
                col[pre] = cnt;
            } while (pre != x);
        }
    }
    pair<int, vector<vector<int>>> rebuild() { // [新图的顶点数量, 新图]
        work();
        vector<vector<int>> adj(cnt + 1);
        for (int i = 0; i < tot; ++i) {
            if (bridge[i]) { // 如果 (i, i ^ 1) 是割边
                int x = col[ver[i]], y = col[ver[i ^ 1]];
                adj[x].push_back(y); // 割边两端点颜色必定不同,故直接连边
            }
        }
        return {cnt, adj};
    }
    void work() {
        for (int i = 1; i <= n; i++) { // 避免图不连通
            if (dfn[i] == 0) {
                tarjan(i, -1);
            }
        }
    }
};

(无向图)割点缩点

概念

割点缩点后的图称为 V-DCC。

求解割点:我们同样维护 timeupper 两个时间戳数组。对于某一条边 \((x-y)\) ,如果满足 time[x] <= upper[y] ,说明 \(y\) 不能到达 \(x\) 的任何一个祖先节点,这时有两种情况,其一是当 \(x\) 不为根节点时,那么显然 \(x\) 是一个割点;其二时当 \(x\) 为根节点时,那么只有当 \(x\) 至少拥有两个子节点时,其才为割点。所以我们还需要维护两个变量,其一是 \(x\) 是否为当前根节点,其二是 \(x\) 的子节点数量。

求解点双:同样借助栈 S 来维护,这里与求解 SCC 和 E-DCC 都不相同——对于某一条边 \((x-y)\) ,只要我们确定了 \(x\) 是割点,就直接开始弹栈操作,一直到 \(y\) 出栈为止,这些点都属于同一个 V-DCC 。而由于下方性质,所以额外的将 \(x\) 也看做属于这个 V-DCC 。除此之外,由于判断条件的变化,我们需要特判:若一个点是孤立点,那么它也是一个 V-DCC 。

点双缩点操作过程:旧图中的每一个割点都是新图中的一个点,旧图中的 V-DCC 都需要向其所包含的每一个割点各连一条边。

struct V_DCC {
    int n;
    vector<vector<int>> ver, col;
    vector<int> dfn, low, S;
    int now, cnt;
    vector<bool> point; // 记录是否为割点

    V_DCC(int n) : n(n) {
        ver.resize(n + 1);
        dfn.resize(n + 1);
        low.resize(n + 1);
        col.resize(2 * n + 1);
        point.resize(n + 1);
        S.clear();
        cnt = now = 0;
    }
    void add(int x, int y) {
        if (x == y) return; // 手动去除重边
        ver[x].push_back(y);
        ver[y].push_back(x);
    }
    void tarjan(int x, int root) {
        low[x] = dfn[x] = now++;
        S.push_back(x);
        if (x == root && !ver[x].size()) { // 特判孤立点
            ++cnt;
            col[cnt].push_back(x);
            return;
        }

        int flag = 0;
        for (auto y : ver[x]) {
            if (!dfn[y]) {
                tarjan(y, root);
                low[x] = min(low[x], low[y]);
                if (dfn[x] <= low[y]) {
                    flag++;
                    if (x != root || flag > 1) {
                        point[x] = true; // 标记为割点
                    }
                    int pre = 0;
                    cnt++;
                    do {
                        pre = S.back();
                        col[cnt].push_back(pre);
                        S.pop_back();
                    } while (pre != y);
                    col[cnt].push_back(x);
                }
            } else {
                low[x] = min(low[x], dfn[y]);
            }
        }
    }
    pair<int, vector<vector<int>>> rebuild() { // [新图的顶点数量, 新图]
        work();
        vector<vector<int>> adj(cnt + 1);
        for (int i = 1; i <= cnt; i++) {
            if (!col[i].size()) { // 注意,孤立点也是 V-DCC
                continue;
            }
            for (auto j : col[i]) {
                if (point[j]) { // 如果 j 是割点
                    adj[i].push_back(point[j]);
                    adj[point[j]].push_back(i);
                }
            }
        }
        return {cnt, adj};
    }
    void work() {
        for (int i = 1; i <= n; ++i) { // 避免图不连通
            if (!dfn[i]) {
                tarjan(i, i);
            }
        }
    }
};

题单

CodeForces 427C - Checkposts (1700):SCC

CodeForces 1811F - Is It Flower? (2100):V-DCC

有其他做法。

CodeForces 118E - Bertown roads (2000):E-DCC

官标 \(\tt 2000\),实际难度应该在 \(\tt 1600\) 左右。一眼模板,最难的点可能在构造输出边部分,然而这个也是典,使用 \(\tt dfs\) 随便处理下即可。

CodeForces 1000E - We Need More Bosses (2100):E-DCC

标签:缩点,AC,ver,int,cnt,DCC,tarjan,dfn,low
From: https://www.cnblogs.com/WIDA/p/17635152.html

相关文章

  • class<T extends interface> 或 class<T extends abstract class>
    packagecom.java3y.austin.test;abstractclassA{publicabstractvoidtest();}classBextendsA{B(){System.out.println("B的构造函数");}@Overridepublicvoidtest(){System.out.println("B的test函数"......
  • stack模拟实现
    stack模拟实现我们用适配器模式/配接器模式,本源是转换,把已有的东西进行转换。设计模式:把常见的设计方法进行总结,适配器也是一种设计模式。我们用已有的容器封装:可以这样定义类模板template<classT,classContainer>,Container就是符合我们要求的一个容器。我们可以将头文件写在......
  • python 运行 execjs 出现错误 UnicodeEncodeError: 'gbk' codec can't encode charac
    异常解读该异常的格式一般为:UnicodeEncodeError: 'gbk'codeccan'tencodecharacter'\xfe'inpositio该异常出现的场景为在Windows电脑下使用Pythonexecjs运行指定的JS文件,但JS文件中包含中文。异常解决方案该问题解决需要修改你电脑Python安装目录下 lib......
  • [usaco2018 jan] sprinklers
    题目农夫约翰有一块很大的田,他正在考虑种甜玉米。经过对他农田的调查,FJ发现它形成了一个(N-1)×(N-1)的正方形。西南角为坐标(0,0),东北角是(N-1,N-1)。在某些整数坐标的位置中有双头喷头,每一个都能够同时喷洒水和肥料。一个在(i,j)处的双头喷头会将水洒在农田中所有在其东面且在其北面的区......
  • Apache ECharts_入门
         ......
  • Apache ECharts_介绍
         ......
  • [React Typescript] Well typed a React Context provider
    importReactfrom"react";import{Equal,Expect}from"../helpers/type-utils";constcreateRequiredContext=<Textendsany>()=>{constcontext=React.createContext<T|null>(null);constuseContext=<Te......
  • AcWing 858. Prim算法求最小生成树
    题目给定一个$n$个点$m$条边的无向图,图中可能存在重边和自环,边权可能为负数。求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。给定一张边带权的无向图$G=(V,E)$,其中$V$表示图中点的集合,$E$表示图中边的集合,$n=|V|,m=|E|$。由$V$中的全部$n$个......
  • [React Typescript] Fixing type inference in a Custom React Hook
    //Problemimport{useState}from"react";import{Equal,Expect}from"../helpers/type-utils";exportconstuseId=(defaultId:string)=>{const[id,setId]=useState(defaultId);return[id,setId];};const[id,setI......
  • 记录一次从surface go2恢复镜像相关的
    终端管理员切换目录不成功在cd后加'/d'合并swm文件【图片】【合并SWM、合并WIM】【wim吧】_百度贴吧(baidu.com)查看swm信息dism/get-wiminfo/wimfile:D:\苏菲\SurfaceGo2_BMR_42132_9.33.4\sources\install.swm导出swm为wimdism/export-image/sourceimagefile......