先介绍一些基础定义:(都是针对无向图)
点连通度与边连通度
在一个无向连通图中,如果有一个顶点集合V,删除顶点集合V以及与V中顶点相连(至少有一端在V中)的所有边后,原图不连通,就称这个点集V为割点集合。
一个图的点连通度:最小割点集合中的顶点数。
如果一个边集合,删除这个边集合后,原图不连通,就称这个边集为割边集合。
一个图的边连通度:最小割边集合中的边数。
点双连通:如果一个无向连通图的点连通度大于1,则称该图是点双连通的,简称双连通或者重连通(即删去任意一点原图任然连通)。
割点:一个图有割点,当且仅当这个图的点连通度为1时,割点集合的唯一元素被称为割点(即删去割点原图不连通),一个图可能有多个割点。
PS:有割点的图不一定有割边。
边双连通:如果一个无向连通图的边连通度大于1,则称该图是边双连通的,简称双连通或者重连通(即删去任意一边原图任然连通)。
桥:一个图有桥,当且仅当这个图的边连通度为1时,割边集合的唯一元素被称为桥(即删去桥原图不连通),一个图可能有多个桥。
PS:有割边的图不一定有割点。
双连通分量:在图G的所有子图G’中,如果G’是双连通的,则称G’是双连通子图。如果一个双连通子图G’,他不是任何一个双连通子图的真子集,则称G’为极大双连通子图,双连通分量或重连通分量。特殊的是点双连通分量又叫做块。
PS:边双连通分量一定是点双连通分量,但点双连通分量不一定是边双连通分量。
在求双连通分量的注意点:
割点可以属于多个点双连通分量,其余点和边只属于一个点双连通分量。
将一个有桥图通过加边变成边双连通分量时:
如果叶子数(缩点后度为1的点)为1,则至少需要添加0条边;
否则为(叶子数+1)/ 2;
判断割点:
一个顶点u是割点,当且仅当满足下列两个条件之一:
1:u为树根,且u有多余一个子树。因为如果只有一颗子树,去掉根节点后肯定不会出现多颗子树,因此不可能为割点;而无向图dfs搜索树中不存在横叉边,所以若有多个子树,这些子树间不会有边相连,因此u肯定是割点。
2:u不为树根,且满足存在(u,v)为树枝边(即u为v在搜索树中的父亲),并使得dfn[u] <= low[v]。(因为删去u后v的子树不能到达u的祖先)。
判断桥
一条无向边(u,v)是桥,当且仅当(u,v)为树枝边且满足dfn[u] < low[v]。(因为v想要到达u的父亲必须经过(u,v)这条边,所以删去这条边,图不连通)。
PS:在判断(u,v)是否有后向边时要注意判断是树枝边的反向边还是新的一条边。
桥模板
求桥://cnt 都从 1 开始
int pre[N],vis[M],sum,bridge[M];
void tarjan(int x){
dfn[x] = low[x] = ++dfstime;
for(int i=head[x];~i;i=nex[i]){
int y = ver[i];
if(!vis[i^1]){
vis[i] = 1;
if(!dfn[y]){
pre[y] = x;
tarjan(y);
low[x] = min(low[x],low[y]);
if(dfn[x] < low[y]){
sum++;
bridge[y] = true;
}
}else low[x] = min(low[x],dfn[y]);
}else vis[i] = 1;
}
}
void solve(){
pre[1] = 1;
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for (int i = 1;i <= n;i++){
if(bridge[i])
printf("%d->%d",i,pre[i]);
}
}
第二版本(用来求删除桥中后边双连通中的点):
void tarjan(int x){
dfn[x] = low[x] = ++dfstime;
st.push(x);
for(int i=head[x];~i;i=nex[i]){
int y = ver[i];
if(!vis[i^1]){
vis[i] = 1;
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x],low[y]);
}else low[x] = min(low[x],dfn[y]);
}else vis[i] = 1;
}
int i;
if(low[x] == dfn[x]){//桥不可能在多个桥双连通里,而点可能在多个点双连通里
colour++;
do{
i=st.top();st.pop();
col[i] = colour; //通桥双连通染色
}while(i!=x);
}
}
模板题:桥
割点模板:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+10;
int head[N],ver[N],nex[N];
int n,m,cnt;
int dfn[N],low[N],dfstime,cut[N];
void add(int a,int b){
ver[++cnt] = b;
nex[cnt] = head[a];
head[a] = cnt;
}
void read(){
memset(head,-1,sizeof head);
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
}
void tarjan(int x,int root){
dfn[x] = low[x] = ++dfstime;
int tot = 0;
for(int i=head[x];~i;i=nex[i]){
int y = ver[i];
if(!dfn[y]){
tot++;
tarjan(y,root);
low[x] = min(low[x],low[y]);
if((x==root && tot>1) || (x!=root && low[y]>= dfn[x])){
cut[x] = 1;
}
}else{
low[x] = min(low[x],dfn[y]);
}
}
}
int main(){
read();
for(int i=1;i<=n;i++) //这是为了防止原图不连通
if(!dfn[i])
tarjan(i,i);
int ans = 0;
for(int i=1;i<=n;i++)
if(cut[i])
ans++;
printf("%d\n",ans);
for(int i=1;i<=n;i++)
if(cut[i])
printf("%d ",i);
return 0;
}
点双连通
int low[N],dfn[N],cut[N];//cut记录割点
int root,dfstime,bt; //bt是一共多少块(多少连通块)
vector<int> block[N];// 连通块里点数
void tarjan(int x,int root){
dfn[x] = low[x] = ++dfstime;
int tot = 0;
st.push(x);
for(int i=head[x];~i;i=nex[i]){
int y = ver[i];
if(!dfn[y]){
tot++;
tarjan(y,root);
low[x] = min(low[x],low[y]);
if((x==root&& tot>1) || (x!=root && dfn[x]<=low[y] )) cut[x] = 1;//求割点的两种判断
if(dfn[x] <= low[y]){
bt ++;
block[bt].clear();
int top;
do{
top = st.top();st.pop();
block[bt].push_back(top);
}while(y!=top);
block[bt].push_back(x); //重点:区分于桥双,点双的点可能多次出现在点双连通中
}
}else{
low[x] = min(low[x],dfn[y]);
}
}
}
int gdnum;//割点总数
void solve(){
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,i);
ll res = 0,num = 1;
for(int i=1;i<=bt;i++){
int gdnum = 0;
int len = block[i].size();
for(int j =0;j<len;j++){
if(cut[block[i][j]]==1)
gdnum ++;
}
}
}
下面给出题目:
例题:边双连通:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+100;
int n,m,cnt=1; //cnt从1开始,保证取反边
int head[N],ver[N],nex[N],colour,top;
int dfn[N],low[N],dfstime,vis[N],du[N],col[N];
stack<int> st;
void add(int a,int b){
ver[++cnt] = b;
nex[cnt] = head[a];
head[a] = cnt;
}
void read(){
memset(head,-1,sizeof head);
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
}
void tarjan(int x){
dfn[x] = low[x] = ++dfstime;
st.push(x);
for(int i=head[x];~i;i=nex[i]){
int y = ver[i];
if(!vis[i^1]){
vis[i] = 1;
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x],low[y]);
}else low[x] = min(low[x],dfn[y]);
}else vis[i] = 1;
}
int i;
if(low[x] == dfn[x]){//桥不可能在多个桥双连通里,而点可能在多个点双连通里
colour++;
do{
i=st.top();st.pop();
col[i] = colour; //通桥双连通染色
}while(i!=x);
}
}
void solve(){
//tarjan(1);
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int x=1;x<=n;x++){
for(int i=head[x];~i;i=nex[i]){
if(vis[i^1]){ //树枝边,因为dfs不考虑双向边
vis[i] = 0;
int y=ver[i];
if(col[y] != col [x]){
du[col[x]]++; //找到叶子节点
du[col[y]]++;
}
}else vis[i] = 0;
}
}
int ans = 0;
for(int i=1;i<=colour;i++){
if(du[i]==1) ans++;
}
printf("%d\n",(ans+1)/2);//新建道路的数目是把原桥删去后桥双连通缩成点后 把每两个叶子上连边即可
//结论:当叶子节点=1,要+的边为0,否则为(叶子数+1)/2
}
int main(){
read();
solve();
return 0;
}
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e3;
int n,m,cs,cnt,root,dfstime,bt;
int nex[N],head[N],ver[N];
int low[N],dfn[N],cut[N];
vector<int> block[N];
stack<int> st;
void add(int x,int y){
ver[++cnt] = y;
nex[cnt] = head[x];
head[x] = cnt;
}
void read(){
for(int i=0;i<N;i++) cut[i] = dfn[i]=low[i]=nex[i] = ver[i] = 0,head[i] = -1;
bt = dfstime = cnt = 0;
while(st.size()) st.pop();
for(int i = 1,x,y;i <= m;i ++){
scanf("%d%d",&x,&y);
n = max(n,max(x,y));//题目未给出最大点数
add(x,y);add(y,x);
}
}
void tarjan(int x,int root){
dfn[x] = low[x] = ++dfstime;
int tot = 0;
st.push(x);
for(int i=head[x];~i;i=nex[i]){
int y = ver[i];
if(!dfn[y]){
tot++;
tarjan(y,root);
low[x] = min(low[x],low[y]);
if((x==root&& tot>1) || (x!=root && dfn[x]<=low[y] )) cut[x] = 1;//求割点的两种判断
if(dfn[x] <= low[y]){
bt ++;
block[bt].clear();
int top;
do{
top = st.top();st.pop();
block[bt].push_back(top);
}while(y!=top);
block[bt].push_back(x); //重点:区分于桥双,点双的点可能多次出现在点双连通中
}
}else{
low[x] = min(low[x],dfn[y]);
}
}
}
void solve(){
ll res = 0,num = 1;
for(int i=1;i<=bt;i++){
int gdnum = 0;
int len = block[i].size();
for(int j =0;j<len;j++){
if(cut[block[i][j]]==1)
gdnum ++;
}
if(gdnum==0) res+=2,num = num*(len-1)*len/2;
else if(gdnum==1) res++,num=num*(len-1);
}
printf("%lld %lld\n",res,num);
}
int main(){
while(~scanf("%d",&m) && m){
printf("Case %d: ",++cs);
read();
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,i);
solve();
}
return 0;
}