无向连通图小结

无向图:桥和割点

桥的概念:无向图删去边e后分裂成两个不相连的子图

割点概念:无向图删去点v以及和v相连的所有边后分裂成两个及以上的子图

一些概念:

  搜索树:在无向图中任意选择一点作为起点进行dfs,每个点访问一次,每次发生递归的边(x,y),即访问到之前没有访问到的点所经过的边,组成的搜索树

  时间戳:在dfs过程中无向图中的结点被访问到的时间,用dfn数组来表示

  追溯值:用low数组表示追溯值,low[x]定义为以下结点的时间戳的最小值:

      1.x 子孙结点的时间戳

      2.x子孙能通过非搜索树上的边所达到的点的时间戳

桥的判定法则:无向边(x,y)是桥 <==> 搜索树上存在x的一个子节点y,有dfn[x]<low[y]

  推论:桥一定是搜索树中的边,一个简单环中的边一定都不是桥

割点的判定法则:如果x是root,那么如果x有两个及以上儿子,x就是割点

        如果x非root,那么如果搜索树上存在x的一个子节点y,有dfs[x]<=low[y],那么x就是割点

 求桥时要特别注意一下重边和父边!

下面是求桥和割点的代码

 

void Tarjan(int u,int pre){
    low[u]=dfn[u]=++index;
    int son=0,pre_cnt=0;//处理重边和回边
    for(int i=head[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(v==pre&&pre_cnt==0){
            pre_cnt++;continue;
        }
        if(!dfn[v]){//如果v未被访问过 
            son++;
            Tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(dfn[u]<low[v]){//
                bridge++;
                edge[i].cut=true;
                edge[i^1].cut=true;
            } 
            if(u!=pre && dfn[u]<=low[v]){//非树根的割点 
                cut[u]=true;
                add_block[u]++;
            }
        }
        else low[u]=min(low[u],dfn[v]);//v被访问过了 
    } 
    if(u==pre && son>1)cut[u]=true;//树根割点 
    if(u==pre)add_block[u]=son-1; 
}

 

无向图:双联通分量

点双联通图:无向连通图不存在割点

边双联通图:无向连通图不存在桥

点双联通分量:无向图的极大点双联通子图v-DCC

边双联通分量:无向图的极大边双联通子图e-DCC

定理

  点双联通图的判定条件:

    1.图的顶点数不超过2

    2.图中任意两点至少包含在一个简单环中

  边双联通图的判定条件:

    1.图中任意一条边都包含在一个简单环中

 

e-DCC的求法:只要求出图中所有桥,把桥删掉后图分裂成的联通块就是e-DCC

先把桥都求出来,然后一次dfs进行染色,数组c[x]表示x所在的联通块的编号

int c[maxn],dcc;
void dfs(int u){
    c[u]=dcc;
    for(int i=head[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(c[v] || bridge[i])continue;//碰到桥就不往下dfs
        dfs(v); 
    }
}
int main(){
    //...
    dcc=0;
    for(int i=1;i<=n;i++)
        if(!c[i]){
            ++dcc;
            dfs(i);
        }
    //...
}

e-DCC的缩点法,把每个边双联通分量缩成一个点,将原图变成一棵树或森林,存在新的邻接表中

这个过程中同样可以求出每个缩点的度数

struct Edge{
    int to,nxt;
}edge_c[maxn<<1];
int head_c[maxn],tot_c;
void init(){
    memset(head_c,-1,sizeof head_c);
    tot_c=0;
}
void add_c(int u,int v){
    edge_c[tot_c].to=v;
    edge_c[tot_c].nxt=head_c[u];
    head_c[u]=tot_c++;
}
int main(){
    //...
    for(int i=0;i<tot;i+=2){
        int v=edge[i].to,u=edge[i^1].to;//无向图的两条边一定连在一起 
        if(c[u]==c[v])continue;
        add_c(c[u],c[v]);
    }
    //...    
}

 

v-DCC的求法:需要在Tarjan算法中维护一个栈

  1.当结点x第一次被访问时,把该节点入栈

  2.当dfn[x]<=low[y]条件成立时,无论x是否为根都要

    a.从栈顶不断弹出结点,直到结点 y 被弹出!!!(注意是y,不用担心同一个联通分量的点会被分成两个)

    b.刚才弹出的所有结点与结点x一起构成一个v-DCC

  开一个vector数组dcc,dcc[i]保存编号为i的v-DCC中的所有结点

vector<int>dcc[maxn];
int cnt;
void tarjan(int x){
    dfn[x]=low[x]=++index;
    stack[++top]=x;
    if(x==root && head[x]==-1){//x是个孤立点 
        dcc[++cnt].push_back(x);
        return;
    }
    int flag=0;
    for(int i=head[x];i!=-1;i=edge[i].nxt){
        int y=edge[i].to;
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]){
                flag++;
                if(x!=root || flag>1)cut[x]=true;//独立判断一次x是割点 
                cnt++;
                int z;
                do{//退栈 
                    z=stack[top--];
                    dcc[cnt].push_back(z);
                }while(z!=y);
                dcc[cnt].push_back(x);//x也要进栈,但是x可能作为割点会被两个及以上联通分量包含,暂时不出栈 
            }
        }
        else low[x]=min(low[x],dfn[y]); 
    }
} 

v-DCC的缩点

设图中有p个割点,t个v-DCC,缩点后的图有p+t个点

把每个DCC和每个割点都作为新图中的结点,并在每个割点与包含它的所有v-DCC之间连边

 先给所有的割点新编号

int main(){
    //...
    num=cnt;
    for(int i=1;i<=n;i++)
        if(cut[x])new_id[x]=++num;//给割点新编号
         
    tot_c=0;
    for(int i=1;i<=cnt;i++)//枚举每个联通分量 
        for(int j=0;j<dcc[i].size();i++){
            int x=dcc[i][j];
            if(cut[x]){//割点和联通分量连边 
                add_c(i,new_id[x]);
                add_c(new_id[x],i); 
            } 
            else c[x]=i;//染色 
        }
    //...
}

 

posted on 2019-03-01 09:41  zsben  阅读(3888)  评论(0编辑  收藏  举报

导航