0阶段-第三日-tarjan与图的连通性

停下了。
今日内容难度较大,题单只做了模板,整合至明日列举,但是对于tarjan算法有了相对深入的了解。接下来整理今日所学。
1、前置
图的DFS树构造出来后:
数组dfn[x]:节点x第一次被访问的时间戳
数组low[x]:节点x所能访问到的点dfn的最小值,这本身是个递归定义(个人认为,后边阐述),且不同类型题目会有有不同定义与限制条件。
前向边:祖先到儿子
后向边:儿子到祖先
横叉边:没有祖先儿子关系
横插边只会指向dfn减小的方向(反证法)
无向图无横叉边 (反证法)
个人习惯于把无向图中前向边和后向边统称回边==。


无向图中:
桥:在一张连通的无向图中,如果将一条边删去后,原图变成不连通的两部分,我们就说这条边是桥。
割点:在一张连通的无向图中,如果将一个点删去后,原图变成不连通的两部分,我们就说这个点是 割点 。
2、tarjan求强连通分量
tatjan算法是通过dfs树遍历每个强连通分量的方式求出每个强连通分量的方法。

把原图G中强连通分量缩点后原图可转化为DAG。接下来我从dfs树和DAG两个视角阐述我对该算法的理解

  • 对G使用dfs,也就是从DAG上任意一缩点开始dfs。为了解释方便,接下来一律用“强连通分量”替代“缩点”一词
  • 易知,DAG上出度为0的强连通分量肯定优先于出度不为0的强连通分量被遍历。宏观上来看这就是逆拓扑排序。
  • 当强连同分量出度为0时(或者本来就为0),接下来肯定会一口气遍历完,这样遍历完这一强连通分量后,我们设置的栈从某一位位置开始到末尾都是该该强连通分量上节点的集合
  • 接下来就是找这一个位置。当然很明显,就是该强连通分量中第一个被访问的节点的位置。
  • 也就是说递归回退到这一节点的位置的时候需要一个东西告诉我们可以退栈,获得相应的强连通分量了。
  • 这个东西就是dfn[n]与low[n]。
    接下来通过dfn与low确定该位置:

low[n]的定义,代码解释更好:

 for(int pre=g1.h[u];pre;pre=g1.e[pre].pre){
        int v=g1.e[pre].to;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(insta[v]) low[u]=min(low[u],dfn[v]);
    }

在栈(insta判断是否在栈中 )和在DAG性质的约束下,其余连同分量的low值和dfn值不会影响到该强连通分量内部的low值得递归传递,约束体现在以下方面:

  • DAG,中指向该强连通分量,不会有指向, 指入该强连通分量的边
  • 从该强连通分量中,指向的相连通分量, 要么已经遍历过,会被insta筛去;
  • 要么会中途进入,但是由于时间戳会更晚,会被去min筛去。

这样就可以把该强连通分量的遍历,单独看作一颗dfs树:从最初进入的节点为根,形成一颗dfs树,该树只包含该强连通分量内部的点与边。
所以个人理解,Low数组的本质是,自叶子至根逐步传递每个节点的min(时间戳,前向边指向的时间戳,横插边指向的时间戳,后向边...,该节点孩子的low)。这样会使得每个节点的low值会小于时间戳,而只有根节点的low值等于时间戳。
这样通过判断时间戳与Low值是否相等,来决定是否全部强连通分量上的节点入栈,然后退栈至根节点(含根节点)即可。
这样算法就成立了,只图理解,不是证明,会有错误。不过对于low,我是做过数据测试的,会出现部分节点发生low[x]!=dfn[dsu[x]]的情况。
3、tarjan算法求割点与桥
比起上面的好理解,
low:

for(int pre=g.h[u];pre;pre=g.e[pre].pre){
        int v=g.e[pre].to;
        if(!dfn[v]){
            tarjan(v,pre);
            low[u]=min(low[u],low[v]);
        }
        else if(pre!=(pre_edge^1) ) low[u]=min(dfn[v],low[u]);
    }

构造dfs树,自叶子至根传递min(该点的时间戳,该点回边对应的时间戳,该点孩子的low)。

  • 如果节点为根节点,如果有两个或以上的的孩子,那么根节点为割点,显然,因为无向图dfs树没有横叉边
  • 如果为非根节点,如果某一个孩子的low值大于等于该节点的时间戳,那个该节点为割点。因为这意味这以该孩子为根的子树上不存在连向该节点祖先的边。
  • 桥和割点差不多,只要把上面的大于等于改为大于即可,甚至不要考虑根节点的问题。当然桥还有树上差分的作法。

补充说明:
为什么强连通分量的一颗dfs树内除了根节点外其余节点的Low值都小于等于dfn值:
解释:
1、对根节点root而言,由于这棵树内最小的的就是dfn[root],所以low[root]=dfn[root]。
2、对于其余节点x而言,以它为根的子树上的节点,至少有一条 指向dfn小于dfn[x]的节点 的横叉边或后向边。否则这个以这个节点为根的树必然自成一个强连通分量,而不属于root所在的强连通分量。解释如下:
3、接下来说的公共祖先是指以x为根的子树上所有节点的公共祖先,规定这里祖先中不包含x本身。
4、上述横叉边可分为两类:子树内部指向或指向非公共祖先节点。如果没有横叉边指向指向dfn小于dfn[x]的节点。即没有边指向非公共祖先节点。同时没有指向dfn小于dfn[x]的节点的后向边,即没有边指向公共祖先节点,那么以x为根的子树必然自称一个强连通分量,而不属于root所在的强连通分量。
5、解释完毕。

补充说明:
为什么强连通分量的一颗dfs树内除了根节点外其余节点的Low值都小于等于dfn值:
解释:
1、对根节点root而言,由于这棵树内最小的的就是dfn[root],所以low[root]=dfn[root]。
2、对于其余节点x而言,以它为根的子树上的节点,至少有一条 指向dfn小于dfn[x]的节点 的横叉边或后向边。否则这个以这个节点为根的树必然自成一个强连通分量,而不属于root所在的强连通分量。解释如下:
3、接下来说的公共祖先是指以x为根的子树上所有节点的公共祖先,规定这里祖先中不包含x本身。
4、上述横叉边可分为两类:子树内部指向或指向非公共祖先节点。如果没有横叉边指向指向dfn小于dfn[x]的节点。即没有边指向非公共祖先节点(横叉边的指向dfn小的节点的性质和dfs的遍历顺序决定)。同时没有指向dfn小于dfn[x]的节点的后向边,即没有边指向公共祖先节点,那么以x为根的子树必然自称一个强连通分量,而不属于root所在的强连通分量。
5、解释完毕。

posted @ 2021-07-01 23:43  七铭的魔法师  阅读(40)  评论(0编辑  收藏  举报