tarjan算法--求解无向图的割点和桥

1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥

    也就是说  无向连通图中,如果删除某边后,图变成不连通,则称该边为桥

2.割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。

求取割点:

  1》当前节点为树根的时候,条件是“要有多余一棵子树”(如果这有一颗子树,去掉这个点也没有影响,如果有两颗子树,去掉这点,两颗子树就不连通了。

  2》当前节点U不是树根的时候,条件是“low[v]>=dfn[u]”,也就是在u之后遍历的点,能够向上翻,最多到u,如果能翻到u的上方,那就有环了,去掉u之后,图仍然连通。保证v向上最多翻到u才可以

树边,前向边,后向边,横叉边

图进行DFS会得到一棵DFS树(森林)

vis = 0,表示该顶点没没有被访问
vis = 1,表示该顶点已经被访问,但其子孙后代还没被访问完,也就没从该点返回
vis = 2,,表示该顶点已经被访问,其子孙后代也已经访问完,也已经从该顶点返回

DFS过程中,对于一条边u->v
vis[v] = 0,说明v还没被访问,v是首次被发现,u->v是一条树边
vis[v] = 1,说明v已经被访问,但其子孙后代还没有被访问完(正在访问中),而u又指向v?说明u就是v的子孙后代(v都访问过了),u->v是一条后向边,因此后向边又称返祖边
vis[v] = 3,z说明v已经被访问,其子孙后代也已经全部访问完,u->v这条边可能是一条横叉边,或者前向边

注意:树边,后向边,前向边,都有祖先,后裔的关系,但横叉边没有,u->v为横叉边,说明在这棵DFS树中,它们不是祖先后裔的关系它们可能是兄弟关系,堂兄弟关系,甚至更远的关系,如果是dfs森林的话,u和v甚至可以在不同的树上

在很多算法中,后向边都是有作用的,但是前向边和横叉边的作用往往被淡化,其实它们没有太大作用。

一直有个疑惑,这一天也就在看这个地方就是优化low数组的时候,如果自己的儿子节点没有被访问过,那么好说我们更新维护low数组是low[u] = min(low[u],low[v])————应付的情况就是子节点通过另一条路径访问到了祖先节点,但是当访问到的字节点被访问过的时候,为什么就要这么更新low数组low[u] = min(low[u],dfn[v])这个我没怎么想明白,后来看到了一个实例:

一个图(v,e)点为1,2,3,4,5,边有(1,2),(2,3),(3,1),(3,4),(4,5),(5,3)令1为树根。显然3为割点。不妨假设搜索顺序是(1,2),(2,3),(3,1),(3,4),(4,5),(5,3)搜索到(3,1)的时候,更新low[3] = dfn[1] = 1后搜索(3,4)、(4,5),(5,3),发现3已经遍历,那么如果此时采用low[u] = min(low[u], low[v])的话,会更新low[5] = low[3] = 1,回溯到4,low[4] = low[5] = 1,回溯到3,low[3] = low[4] = 1,然后比较发现low[4] < dfn[3],判断出3不是割点,算法错误。

所以也就是为了避免那个被访问过的点已经属于了一个联通分量了这样的话就会造成错误的联通分量融合的情况,这样也只是表面上明白了,还没有完全透彻,后续再捂捂

初始准备

const int maxn = 20010;
int n;
struct node{
    int to,pre;
}e[maxn];
int id[maxn],cnt;
int index;
int low[maxn],dfn[maxn];
int cut_point[maxn];

 链式前向星存储边 + index模拟时间戳 + low数组表示的意思是与"u节点及其子孙节点"相连的最先被访问到的点的访问序号。表示u节点最早可从那个节点访问到 + dfn数组为这个点的dfs访问次序

初始化过程

void init()
{
    cnt = 0;
    index = 0;
    memset(low,0,sizeof(low));
    memset(dfn,0,sizeof(dfn));
    memset(id,-1,sizeof(id));
    memset(cut_point,0,sizeof(cut_point));
}

加边函数

void add(int from,int to)
{
    e[cnt].to = to;
    e[cnt].pre = id[from];
    id[from] = cnt++;
}

 tarjan算法

注释也差不多啦

访问一个点的时候初始化两个数组,记录根节点的子树数目,对于没有访问过的回溯时两种方法判断是否为为割点

void tarjan(int u,int fa)
{
    int son = 0;
    dfn[u] = low[u] = ++index;
    for(int i = id[u];~i;i = e[i].pre)
    {
        int v = e[i].to;
        if(!dfn[v])//目标点没有被访问过
        {
            tarjan(v,u);//先访问
            son++;//记录儿子数目

            low[u] = min(low[u],low[v]);//更新回溯的值

            if(dfn[u] <= low[v] && u != 1)//如果不是数根,而且子节点回不到父节点及其以上
                cut_point[u] = 1;
            if(u == 1 && son > 1)//对于根节点,如果有两个及其以上的子树,那么肯定是割点
                cut_point[u] = 1;
        }
        else//目标点被访问过,有两种可能
            //1.从开始点一个环过来的,很简单对于low数组的更新都一样
            //2.从另一个点延伸过来的,那就代表被访问过的点的low数组已经
            //被那个强连通分量更新了,所以我们采用他的访问次数进行更新
            low[u] = min(low[u],dfn[v]);
    }
}

 好了接下来就是根据题目更新了

int get_cnt()
{
    int ans = 0;
    tarjan(1,1);
    for(int i = 1;i <= n;i++)
    {
        if(cut_point[i])
            ans++;
    }
    return ans;
}
int main()
{
    while(~scanf("%d",&n),n)
    {
        init();
        int s,t;
        while(~scanf("%d",&s),s)
        {
            while(getchar() != '
')
            {
                scanf("%d",&t);
                add(s,t);
                add(t,s);
            }
        }
        printf("%d
",get_cnt());
    }
    return 0;
}
原文地址:https://www.cnblogs.com/DF-yimeng/p/9403626.html