试着用一段代码解决图论的几个基本的连通性问题:
关键词:DFS,Tarjan,邻接表
- 全图连通分量,是否有,有的话,求个数
- 求关节点/割点并输出其贡献连通分量个数
- 求桥,并按照顺序输出
- 强连通域分解并输出:Tarjan ,//也可用Kosaraju算法
- 边双连通分支:有桥的连通图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;
}