图论1--连通性问题的整理

试着用一段代码解决图论的几个基本的连通性问题:

关键词:DFS,Tarjan,邻接表

  1. 全图连通分量,是否有,有的话,求个数
  2. 求关节点/割点并输出其贡献连通分量个数
  3. 求桥,并按照顺序输出
  4. 强连通域分解并输出:Tarjan ,//也可用Kosaraju算法
  5. 边双连通分支:有桥的连通图G,增加最少的边,构造成双连通图;

代码如下:

//
//  main.cpp
//  图论-连通问题整理
//
//  Created by Cordelia.LIU on 2019/11/6.
//  Copyright © 2019 Cordelia.LIU All rights reserved.
//



/*一个综合的图的连通问题
 1.关键
    1)求割点
            dfs/是关节点判断的两个条件:1)独子根,if(u==root&&son>1) u是关节点
                                2)非根的最高祖先 ,uv是树边(就是父子关系)low[v]>=dfs[u];u是关节点
    2)求割边
            uv是树边(就是父子关系)low[v]>dfs[u];
    3)求一共几个连通域
            在函数上层设,上层的dfs几次就有几个连通域
    4)求删掉每个点后增加的连通域块(if 关节点+1,ifnot 不加)
    
 
 2.技巧
    1)存图的技巧 addedge 1)检查重变2)快速遍历
    2)几个重要的数组 dfs[]. low[]
    3) 求割边的时候有时候要检查重边 //
        重边编号
 
 */



#include <iostream>
#include <cstring>
#include<algorithm>
#include <stack>
#include <vector>
using namespace std;

const int MAX=100;
int dfs[MAX];//存点在dfs中的编号,相当于dtime
int low[MAX]; //语义是节点i以及i的子孙,通过非父亲节点能访问到的最高的祖先,随着dfs不断更新
int from[MAX];//存树边的来边,也就是父亲;
int bridge;//存桥的数量
int add_block[MAX];//删除某个节点之后,令全图连通域增加多少
int total;//记录边的总数;
int depth;//记录当前的遍历深度
int cnt;//一共几个连通域
int articucount;//一个关节点
int degree[MAX];//存每个边双连通分量凝缩成点后度数(1桥,+1度)
int block[MAX];//每个双连通分量包含的桥数;(要通过关节点双连通域分解去求,见任务4)
vector<vector<int> > component;//存放一组组连通域'
vector<int> cuts;//存放割点集

int head[MAX];//存边e在的顶点a,一共有的邻边个数,next始终比其在head中少1;
stack<int> s;



struct Edge {
    int a;
    int b;//a是起点,b是终点,
    int next; //邻边的编号索引
    bool cut;//标记是不是割点
    Edge():next(-1),cut(0){};//注意初始化
    Edge(int a,int b,int id):next(-1),cut(0){};//id初始化都为-1,这样遍历的时候检查id==-1就可以知道是不是有没有边了
    //处理方法2是存edges存Edge*, 初始化成NULL,检查是NULL就说明没有边;
    bool operator<(const Edge&e)const{
        return a<e.a?true:b<e.b;
    }
    friend ostream & operator<<(ostream& os,Edge & e){
    os<<e.a<<"->"<<e.b<<"\t";
    return os;
    }

};

Edge edges[MAX*MAX]; //注意,这个edges要开的大一点;
vector<Edge> bridges;



void init(){//和测试case有关的应该在这一层初始化,和内部实现有关的应该在内层初始化
    memset(dfs,-1,sizeof(dfs));//dfs初始化为-1.这样-1就可以检查是否被访问
    memset(head,-1,sizeof(head));//这样第一个的next才会是-1
    memset(add_block,0,sizeof(add_block));
    memset(degree,0,sizeof(degree));
    memset(block,0,sizeof(block));
    for(int i=0;i<MAX;i++){
        from[i]=i;//将每个边的from存成自己;
    }
    bridge=0;
    cnt=0;
    total=0;
    depth=0;//记录当前的遍历深度
    articucount=0;
    component.clear();
        bridges.clear();
        cuts.clear();
    while(!s.empty()) s.pop();
        
        
        
}

void addedge(int a,int b){//total是存边的个数,全局变量//⚠️ addedge是从0开始的,一般的点编号是从1开始的,中间有一个偏移量
    edges[total].b=b;
    edges[total].next=head[a];                      //0,     1    2
    head[a]=total++;//这一步处理,使得每一个head【a】存的是最后一条边在tedges中的索引号,之前的边要通过每个变得next去遍历
    //遍历是从后往前遍历
}


void Tarjan(int u,int father){//u是传入的节点,father是u的父亲的编号//初始的时候传入Tarjan(u,u),
    dfs[u]=depth++;
    low[u]=dfs[u];//每个未被访问的节点初始化都是用自己的dfs
    from[u]=father;
    s.push(u);
    int son=0;//计算根节点有几颗子树
    int dupliedge=0;//重边判断,在每一次tarjan的时候先归0
    for(int i=head[u];i!=-1;i=edges[i].next){//看这个遍历,i应该是这遍历边的edges的索引
        int v=edges[i].b;
        //处理重边 !!
        if(v==father&&dupliedge==0) {
            dupliedge++;
            continue;
        }//这时候,如果v是重复的边,那么仍会回进行下面的操作;
        if(dfs[v]==-1){//处理树边
            son++;//注意,svon要在递归之前
            Tarjan(v,u);//等到递归回来时
            low[u]=min(low[v],low[u]);//更新其子孙所能得到访问到的最高祖先
            if(u!=father&&low[v]>=dfs[u]){//割点判断;
                cuts.push_back(u);
                add_block[u]++;
                articucount++;
                    
            }
            if(low[v]>dfs[u]){
                bridge++;//此边为割桥
                edges[i].cut=true;
                if(u<v){
                    bridges.push_back(edges[i]);//避免重复
                }
                
            }
        }
        else{//处理回向边,这个时候如果有重边的话就会干扰,此时v已经被访问过了,所以之前处理了
            low[u]=min(low[u],dfs[v]);
        }
        if(u==father&&son>1){//如果u是根且有多于1的分支
            cuts.push_back(u);
            add_block[u]=son-1;//⚠️问什么是son-1 ,因为语义是增加的块,
        }
    
    }
}

void solve(int N){//解决有N的顶点的问题;
    init();
    for(int i=0;i<N;i++){
        if(!dfs[i]){
            cnt++;
            Tarjan(i,i);//上层调用
        }
    }
    //任务1:输出连通域个数
    cout<<"连通域个数为"<<cnt<<endl;//具体看题要求,如果给你了一个连通图,那么这个cnt就没有必要
        cout<<endl;
        
    //任务2:输出割点,以及删掉他会导致连通域增加的个数
    cout<<"cut集合:"<<endl;
    for(auto e:cuts){
            cout<<e<<"\t贡献连通域数量 "<<add_block[e]<<endl;
    }
         cout<<endl;
    //任务3:按顺序输出桥,注意不要重复输出
    cout<<"桥一共有"<<bridge<<"个"<<endl;
        sort(bridges.begin(),bridges.end());
        for(auto e:bridges){
        cout<<e;
        }
        
        cout<<endl;
    //任务4:双连通域分解
        cout<<"双连通域分解后输出各个连通域"<<endl;
        int k=0;
        int t=articucount-1;
        while(!s.empty()){
            if(t){
                while(s.top()!=cuts[t]){
                    int temp=s.top();
                    block[temp]=k;//记录每个点属于的block,为任务5铺垫
                    component[k].push_back(temp);
                    s.pop();
                }
                component[k].push_back(cuts[t]);
                t--;
                k++;
            }
            else{//到最后一轮连通域了;
                int temp=s.top();
                block[temp]=k;
                component[k].push_back(temp);
                s.pop();
            }
        }
            
    //输出连通域
        int blocks=++articucount;
        for(int i=0;i<blocks;i++){
            cout<<"#"<<i<<")"<<"\t";
            for(auto e:component[i]){
                cout<<e<<" ";
            }
        }
        cout<<endl;
    
    //任务5;边双连通域分支;凝点->叶节点判断->(leaf+1)/2
    for(int i=0;i<N;i++){
        for(int j=head[i];j!=-1;j=edges[i].next){
            if(edges[j].cut){
                degree[block[i]]++;//i所在块的凝缩后的点度数+1
            }
        }
    }
    //没写bool cut[]数组, 写了一个这一步遍历可以简化到O(N);
    int leaf=0;
    for(int i=0;i<blocks;i++){
        if(degree[i]==2){//没排关节点,利用cut数组可以简化;==1
            leaf++;
        }
    }
    cout<<"最少需要增加"<<(leaf+1)/2<<"条边,将原图构造成双连通域"<<endl;
    
    
    
}


//主函数,负责读图,调用solve即可
int main(int argc, const char * argv[]) {
        int T;//组数
        cin>>T;
        while(T--){
            int N,a,b;
            cin>>N;
            while(cin>>a>>b){
                addedge(a,b);
                addedge(b,a);//无向图
            //假设编号都是从0开始
            }
            solve(N);
        }
        

    return 0;
}
发布了2 篇原创文章 · 获赞 1 · 访问量 31

猜你喜欢

转载自blog.csdn.net/weixin_41749176/article/details/102939259
今日推荐